pax_global_header00006660000000000000000000000064144175504310014516gustar00rootroot0000000000000052 comment=da582ed180ca768052410105b5e4d4394f80e674 gocb-2.6.3/000077500000000000000000000000001441755043100124405ustar00rootroot00000000000000gocb-2.6.3/.gitignore000066400000000000000000000000141441755043100144230ustar00rootroot00000000000000*~ .projectgocb-2.6.3/.golangci.yml000066400000000000000000000006531441755043100150300ustar00rootroot00000000000000run: tests: false skip-files: - logging.go # Logging has some utility functions that are useful to have around which get flagged up linters: enable: - bodyclose - revive - gosec - unconvert linters-settings: revive: set-exit-status: true min-confidence: 0.81 rules: - name: var-naming arguments: [["URL"]] errcheck: check-type-assertions: true check-blank: true gocb-2.6.3/CONTRIBUTING.md000066400000000000000000000106511441755043100146740ustar00rootroot00000000000000# Contributing In addition to filing bugs, you may contribute by submitting patches to fix bugs in the library. Contributions may be submitting to . We use Gerrit as our code review system - and thus submitting a change requires an account there. Note that pull requests will not be ignored but will be responded to more quickly and with more detail in Gerrit. For something to be accepted into the codebase, it must be formatted properly and have undergone proper testing. We use `golangci` for linting with a number of linters enabled. To install and use the linter you can use `make devsetup` and `make lint`. You can also run the linter and test suite together using `make check`. Please note that we keep the linting tools out of the `go.mod` file. ## Branches and Tags Released versions of the library are marked as annotated tags inside the repository. * The `master` branch represents the mainline branch. The master branch typically consists of content going into the next release. ## Contributing Patches If you wish to contribute a new feature or a bug fix to the library, try to follow the following guidelines to help ensure your change gets merged upstream. ### Before you begin For any code change, ensure the new code you write looks similar to the code surrounding it and that linting does not produce errors. If your change is going to involve a substantial amount of time or effort, please attempt to discuss it with the project developers first who will provide assistance and direction where possible. #### For new features Ensure the feature you are adding does not already exist, and think about how this feature may be useful for other users. In general less intrusive changes are more likely to be accepted. #### For fixing bugs Ensure the bug you are fixing is actually a bug (and not a usage error), and that it has not been fixed in a more recent version. Please read the release notes as well as the issue tracker to see a list of open and resolved issues. ### Code Review #### Signing up on Gerrit Everything that is merged into the library goes through a code review process. The code review process is done via [Gerrit](http://review.couchbase.org). To sign up for a gerrit account, go to http://review.couchbase.org and click on the _Register_ link at the top right. Once you've signed in you will need to agree to the CLA (Contributor License Agreement) by going you your gerrit account page and selecting the _Agreements_ link on the left. When you've done that, everything should flow through just fine. Be sure that you have registered your email address at http://review.couchbase.org/#/settings/contact as many sign-up methods won't pass emails along. Note that your email address in your code commit and in the gerrit settings must match. Add your public SSH key to gerrit before submitting. #### Setting up your fork with Gerrit Assuming you have a repository created like so: ``` $ git clone https://github.com/couchbase/gocb.git ``` you can simply perform two simple steps to get started with gerrit: ``` $ git remote add gerrit ssh://${USERNAME}@review.couchbase.org:29418/gocb $ scp -P 29418 ${USERNAME}@review.couchbase.org:hooks/commit-msg .git/hooks $ chmod a+x .git/hooks/commit-msg ``` The last change is required for annotating each commit message with a special header known as `Change-Id`. This allows Gerrit to group together different revisions of the same patch. #### Pushing a changeset Now that you have your change and a gerrit account to push to, you need to upload the change for review. To do so, invoke the following incantation: ``` $ git push gerrit HEAD:refs/for/master ``` Where `gerrit` is the name of the _remote_ added earlier. You may encounter some errors when pushing. The most common are: * "You are not authorized to push to this repository". You will get this if your account has not yet been approved. Feel free to ask about in gitter.im/couchbase or in the forums for help if blocked. * "Missing Change-Id". You need to install the `commit-msg` hook as described above. Note that even once you do this, you will need to ensure that any prior commits already have this header - this may be done by doing an interactive rebase (e.g. `git rebase -i origin/master` and selecting `reword` for all the commits; which will automatically fillin the Change-Id). Once you've pushed your changeset you can add people to review. Currently these are: * Charles Dixon * Brett Lawson gocb-2.6.3/LICENSE000066400000000000000000000261361441755043100134550ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gocb-2.6.3/Makefile000066400000000000000000000024511441755043100141020ustar00rootroot00000000000000devsetup: go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.39.0 go get github.com/vektra/mockery/.../ git submodule update --remote --init --recursive test: go test ./ fasttest: go test -short ./ cover: go test -coverprofile=cover.out ./ lint: golangci-lint run -v check: lint go test -short -cover -race ./ bench: go test -bench=. -run=none --disable-logger=true updatetestcases: git submodule update --remote --init --recursive updatemocks: mockery --name=connectionManager --output=. --testonly --inpackage mockery --name=kvProvider --output=. --testonly --inpackage mockery --name=httpProvider --output=. --testonly --inpackage mockery --name=diagnosticsProvider --output=. --testonly --inpackage mockery --name=mgmtProvider --output=. --testonly --inpackage mockery --name=analyticsProvider --output=. --testonly --inpackage mockery --name=queryProvider --output=. --testonly --inpackage mockery --name=searchProvider --output=. --testonly --inpackage mockery --name=viewProvider --output=. --testonly --inpackage mockery --name=waitUntilReadyProvider --output=. --testonly --inpackage mockery --name=kvCapabilityVerifier --output=. --testonly --inpackage # pendingOp is manually mocked .PHONY: all test devsetup fasttest lint cover check bench updatetestcases updatemocks gocb-2.6.3/README.md000066400000000000000000000043701441755043100137230ustar00rootroot00000000000000[![GoDoc](https://godoc.org/github.com/couchbase/gocb?status.png)](https://pkg.go.dev/github.com/couchbase/gocb) # Couchbase Go Client The Go SDK library allows you to connect to a Couchbase cluster from Go. It is written in pure Go, and uses the included gocbcore library to handle communicating to the cluster over the Couchbase binary protocol. ## Useful Links ### Source The project source is hosted at [https://github.com/couchbase/gocb](https://github.com/couchbase/gocb). ### Documentation You can explore our API reference through godoc at [https://pkg.go.dev/github.com/couchbase/gocb](https://pkg.go.dev/github.com/couchbase/gocb). You can also find documentation for the Go SDK on the [official Couchbase docs](https://docs.couchbase.com/go-sdk/current/hello-world/overview.html). ### Bug Tracker Issues are tracked on Couchbase's public [issues.couchbase.com](http://www.couchbase.com/issues/browse/GOCBC). Contact [the site admins](https://issues.couchbase.com/secure/ContactAdministrators!default.jspa) regarding login or other problems at issues.couchbase.com (officially) or ask around [on the forum](https://forums.couchbase.com/) (unofficially). ### Discussion You can chat with us on [Discord](https://discord.com/invite/sQ5qbPZuTh) or the [official Couchbase forums](https://forums.couchbase.com/c/go-sdk/23). ## Installing To install the latest stable version, run: ```bash go get github.com/couchbase/gocb/v2@latest ``` To install the latest developer version, run: ```bash go get github.com/couchbase/gocb/v2@master ``` ## Testing You can run tests in the usual Go way: `go test -race ./...` Which will execute both the unit test suite and the integration test suite. By default, the integration test suite is run against a mock Couchbase Server. See the `testmain_test.go` file for information on command line arguments for running tests against a real server instance. ## Release train Releases are targeted for every third Tuesday of the month. This is subject to change based on priorities. ## Linting Linting is performed used `golangci-lint`. To run: `make lint` ## License Copyright 2016 Couchbase Inc. Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/couchbase/gocb/blob/master/LICENSE) for further details. gocb-2.6.3/analyticsindexes_links.go000066400000000000000000000176661441755043100175560ustar00rootroot00000000000000package gocb import ( "net/url" "strings" ) // CouchbaseRemoteAnalyticsEncryptionSettings are the settings available for setting encryption level on a link. type CouchbaseRemoteAnalyticsEncryptionSettings struct { // The level of encryption to apply, defaults to none EncryptionLevel AnalyticsEncryptionLevel // The certificate to use when encryption level is set to full. // This must be set if encryption level is set to full. Certificate []byte // The client certificate to use when encryption level is set to full. // This cannot be used if username and password are also used. ClientCertificate []byte // The client key to use when encryption level is set to full. // This cannot be used if username and password are also used. ClientKey []byte } // CouchbaseRemoteAnalyticsLink describes a remote analytics link which uses a Couchbase data service that is not part // of the same cluster as the Analytics Service. type CouchbaseRemoteAnalyticsLink struct { Dataverse string LinkName string Hostname string Encryption CouchbaseRemoteAnalyticsEncryptionSettings Username string Password string } // Name returns the name of this link. func (al *CouchbaseRemoteAnalyticsLink) Name() string { return al.LinkName } // DataverseName returns the name of the dataverse that this link belongs to. func (al *CouchbaseRemoteAnalyticsLink) DataverseName() string { return al.Dataverse } // LinkType returns the type of analytics type this link is: AnalyticsLinkTypeCouchbaseRemote. func (al *CouchbaseRemoteAnalyticsLink) LinkType() AnalyticsLinkType { return AnalyticsLinkTypeCouchbaseRemote } // FormEncode encodes the link into a form data representation, to sent as the body of a CreateLink or ReplaceLink // request. func (al *CouchbaseRemoteAnalyticsLink) FormEncode() ([]byte, error) { data := url.Values{} data.Add("hostname", al.Hostname) data.Add("type", string(AnalyticsLinkTypeCouchbaseRemote)) if al.Username != "" { data.Add("username", al.Username) } if al.Password != "" { data.Add("password", al.Password) } if !strings.Contains(al.Dataverse, "/") { data.Add("dataverse", al.Dataverse) data.Add("name", al.LinkName) } data.Add("encryption", al.Encryption.EncryptionLevel.String()) if len(al.Encryption.Certificate) > 0 { data.Add("certificate", string(al.Encryption.Certificate)) } if len(al.Encryption.ClientCertificate) > 0 { data.Add("clientCertificate", string(al.Encryption.ClientCertificate)) } if len(al.Encryption.ClientKey) > 0 { data.Add("clientKey", string(al.Encryption.ClientKey)) } return []byte(data.Encode()), nil } // Validate is used by CreateLink and ReplaceLink to ensure that the link is valid. func (al *CouchbaseRemoteAnalyticsLink) Validate() error { if al.Dataverse == "" { return makeInvalidArgumentsError("dataverse must be set for couchbase analytics links") } if al.LinkName == "" { return makeInvalidArgumentsError("name must be set for couchbase analytics links") } if al.Hostname == "" { return makeInvalidArgumentsError("hostname must be set for couchbase analytics links") } if al.Encryption.EncryptionLevel == AnalyticsEncryptionLevelFull && len(al.Encryption.Certificate) == 0 { return makeInvalidArgumentsError("when encryption level is full a certificate must be set for couchbase analytics links") } if (len(al.Encryption.ClientKey) > 0 && len(al.Encryption.ClientCertificate) == 0) || (len(al.Encryption.ClientKey) == 0 && len(al.Encryption.ClientCertificate) > 0) { return makeInvalidArgumentsError("client certificate and client key must be set together for couchbase analytics links") } return nil } // S3ExternalAnalyticsLink describes an external analytics link which uses the AWS S3 service to access data. type S3ExternalAnalyticsLink struct { Dataverse string LinkName string AccessKeyID string SecretAccessKey string // SessionToken is only available in 7.0+. SessionToken string Region string ServiceEndpoint string } // Validate is used by CreateLink and ReplaceLink to ensure that the link is valid. func (al *S3ExternalAnalyticsLink) Validate() error { if al.Dataverse == "" { return makeInvalidArgumentsError("dataverse must be set for s3 analytics links") } if al.LinkName == "" { return makeInvalidArgumentsError("name must be set for s3 analytics links") } if al.AccessKeyID == "" { return makeInvalidArgumentsError("access key id must be set for s3 analytics links") } if al.SecretAccessKey == "" { return makeInvalidArgumentsError("secret access key must be set for s3 analytics links") } if al.Region == "" { return makeInvalidArgumentsError("region must be set for s3 analytics links") } return nil } // Name returns the name of this link. func (al *S3ExternalAnalyticsLink) Name() string { return al.LinkName } // DataverseName returns the name of the dataverse that this link belongs to. func (al *S3ExternalAnalyticsLink) DataverseName() string { return al.Dataverse } // LinkType returns the type of analytics type this link is: AnalyticsLinkTypeS3External. func (al *S3ExternalAnalyticsLink) LinkType() AnalyticsLinkType { return AnalyticsLinkTypeS3External } // FormEncode encodes the link into a form data representation, to sent as the body of a CreateLink or ReplaceLink // request. func (al *S3ExternalAnalyticsLink) FormEncode() ([]byte, error) { data := url.Values{} if !strings.Contains(al.Dataverse, "/") { data.Add("dataverse", al.Dataverse) data.Add("name", al.LinkName) } data.Add("type", string(AnalyticsLinkTypeS3External)) data.Add("accessKeyId", al.AccessKeyID) data.Add("secretAccessKey", al.SecretAccessKey) data.Add("region", al.Region) if al.SessionToken != "" { data.Add("sessionToken", al.SessionToken) } if al.ServiceEndpoint != "" { data.Add("serviceEndpoint", al.ServiceEndpoint) } return []byte(data.Encode()), nil } // AzureBlobExternalAnalyticsLink describes an external analytics link which uses the Microsoft Azure Blob Storage // service. // Only available as of 7.0 Developer Preview. // VOLATILE: This API is subject to change at any time. type AzureBlobExternalAnalyticsLink struct { Dataverse string LinkName string ConnectionString string AccountName string AccountKey string SharedAccessSignature string BlobEndpoint string EndpointSuffix string } // Name returns the name of this link. func (al *AzureBlobExternalAnalyticsLink) Name() string { return al.LinkName } // DataverseName returns the name of the dataverse that this link belongs to. func (al *AzureBlobExternalAnalyticsLink) DataverseName() string { return al.Dataverse } // LinkType returns the type of analytics type this link is: AnalyticsLinkTypeAzureExternal. func (al *AzureBlobExternalAnalyticsLink) LinkType() AnalyticsLinkType { return AnalyticsLinkTypeAzureExternal } // Validate is used by CreateLink and ReplaceLink to ensure that the link is valid. func (al *AzureBlobExternalAnalyticsLink) Validate() error { if al.Dataverse == "" { return makeInvalidArgumentsError("dataverse must be set for azureblob analytics links") } if al.LinkName == "" { return makeInvalidArgumentsError("name must be set for azureblob analytics links") } return nil } // FormEncode encodes the link into a form data representation, to sent as the body of a CreateLink or ReplaceLink // request. func (al *AzureBlobExternalAnalyticsLink) FormEncode() ([]byte, error) { data := url.Values{} data.Add("type", string(AnalyticsLinkTypeAzureExternal)) if al.ConnectionString != "" { data.Add("connectionString", al.ConnectionString) } if al.AccountName != "" { data.Add("accountName", al.AccountName) } if al.AccountKey != "" { data.Add("accountKey", al.AccountKey) } if al.SharedAccessSignature != "" { data.Add("sharedAccessSignature", al.SharedAccessSignature) } if al.BlobEndpoint != "" { data.Add("blobEndpoint", al.BlobEndpoint) } if al.EndpointSuffix != "" { data.Add("endpointSuffix", al.EndpointSuffix) } return []byte(data.Encode()), nil } gocb-2.6.3/analyticsquery_options.go000066400000000000000000000055021441755043100176210ustar00rootroot00000000000000package gocb import ( "context" "strings" "time" "github.com/google/uuid" ) // AnalyticsScanConsistency indicates the level of data consistency desired for an analytics query. type AnalyticsScanConsistency uint const ( // AnalyticsScanConsistencyNotBounded indicates no data consistency is required. AnalyticsScanConsistencyNotBounded AnalyticsScanConsistency = iota + 1 // AnalyticsScanConsistencyRequestPlus indicates that request-level data consistency is required. AnalyticsScanConsistencyRequestPlus ) // AnalyticsOptions is the set of options available to an Analytics query. type AnalyticsOptions struct { // ClientContextID provides a unique ID for this query which can be used matching up requests between connectionManager and // server. If not provided will be assigned a uuid value. ClientContextID string // Priority sets whether this query should be assigned as high priority by the analytics engine. Priority bool PositionalParameters []interface{} NamedParameters map[string]interface{} Readonly bool ScanConsistency AnalyticsScanConsistency // Raw provides a way to provide extra parameters in the request body for the query. Raw map[string]interface{} Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } func (opts *AnalyticsOptions) toMap() (map[string]interface{}, error) { execOpts := make(map[string]interface{}) if opts.ClientContextID == "" { execOpts["client_context_id"] = uuid.New().String() } else { execOpts["client_context_id"] = opts.ClientContextID } if opts.ScanConsistency != 0 { if opts.ScanConsistency == AnalyticsScanConsistencyNotBounded { execOpts["scan_consistency"] = "not_bounded" } else if opts.ScanConsistency == AnalyticsScanConsistencyRequestPlus { execOpts["scan_consistency"] = "request_plus" } else { return nil, makeInvalidArgumentsError("unexpected consistency option") } } if opts.PositionalParameters != nil && opts.NamedParameters != nil { return nil, makeInvalidArgumentsError("positional and named parameters must be used exclusively") } if opts.PositionalParameters != nil { execOpts["args"] = opts.PositionalParameters } if opts.NamedParameters != nil { for key, value := range opts.NamedParameters { if !strings.HasPrefix(key, "$") { key = "$" + key } execOpts[key] = value } } if opts.Readonly { execOpts["readonly"] = true } if opts.Raw != nil { for k, v := range opts.Raw { execOpts[k] = v } } return execOpts, nil } gocb-2.6.3/asyncopmanager.go000066400000000000000000000016111441755043100157750ustar00rootroot00000000000000package gocb import ( "context" gocbcore "github.com/couchbase/gocbcore/v10" ) type asyncOpManager struct { signal chan struct{} cancelCh chan struct{} wasResolved bool ctx context.Context } func (m *asyncOpManager) SetCancelCh(cancelCh chan struct{}) { m.cancelCh = cancelCh } func (m *asyncOpManager) Reject() { m.signal <- struct{}{} } func (m *asyncOpManager) Resolve() { m.wasResolved = true m.signal <- struct{}{} } func (m *asyncOpManager) Wait(op gocbcore.PendingOp, err error) error { if err != nil { return err } select { case <-m.signal: // Good to go case <-m.ctx.Done(): op.Cancel() <-m.signal case <-m.cancelCh: op.Cancel() <-m.signal } return nil } func newAsyncOpManager(ctx context.Context) *asyncOpManager { if ctx == nil { ctx = context.Background() } return &asyncOpManager{ signal: make(chan struct{}, 1), ctx: ctx, } } gocb-2.6.3/auth.go000066400000000000000000000106361441755043100137360ustar00rootroot00000000000000package gocb import ( "crypto/tls" gocbcore "github.com/couchbase/gocbcore/v10" ) // UserPassPair represents a username and password pair. // VOLATILE: This API is subject to change at any time. type UserPassPair gocbcore.UserPassPair // AuthCredsRequest encapsulates the data for a credential request // from the new Authenticator interface. // VOLATILE: This API is subject to change at any time. type AuthCredsRequest struct { Service ServiceType Endpoint string } // AuthCertRequest encapsulates the data for a certificate request // from the new Authenticator interface. // VOLATILE: This API is subject to change at any time. type AuthCertRequest struct { Service ServiceType Endpoint string } // Authenticator provides an interface to authenticate to each service. Note that // only authenticators implemented via the SDK are stable. type Authenticator interface { // VOLATILE: This API is subject to change at any time. SupportsTLS() bool // VOLATILE: This API is subject to change at any time. SupportsNonTLS() bool // VOLATILE: This API is subject to change at any time. Certificate(req AuthCertRequest) (*tls.Certificate, error) // VOLATILE: This API is subject to change at any time. Credentials(req AuthCredsRequest) ([]UserPassPair, error) } // PasswordAuthenticator implements an Authenticator which uses an RBAC username and password. type PasswordAuthenticator struct { Username string Password string } // SupportsTLS returns whether this authenticator can authenticate a TLS connection. // VOLATILE: This API is subject to change at any time. func (ra PasswordAuthenticator) SupportsTLS() bool { return true } // SupportsNonTLS returns whether this authenticator can authenticate a non-TLS connection. // VOLATILE: This API is subject to change at any time. func (ra PasswordAuthenticator) SupportsNonTLS() bool { return true } // Certificate returns the certificate to use when connecting to a specified server. // VOLATILE: This API is subject to change at any time. func (ra PasswordAuthenticator) Certificate(req AuthCertRequest) (*tls.Certificate, error) { return nil, nil } // Credentials returns the credentials for a particular service. // VOLATILE: This API is subject to change at any time. func (ra PasswordAuthenticator) Credentials(req AuthCredsRequest) ([]UserPassPair, error) { return []UserPassPair{{ Username: ra.Username, Password: ra.Password, }}, nil } // CertificateAuthenticator implements an Authenticator which can be used with certificate authentication. type CertificateAuthenticator struct { ClientCertificate *tls.Certificate } // SupportsTLS returns whether this authenticator can authenticate a TLS connection. // VOLATILE: This API is subject to change at any time. func (ca CertificateAuthenticator) SupportsTLS() bool { return true } // SupportsNonTLS returns whether this authenticator can authenticate a non-TLS connection. // VOLATILE: This API is subject to change at any time. func (ca CertificateAuthenticator) SupportsNonTLS() bool { return false } // Certificate returns the certificate to use when connecting to a specified server. // VOLATILE: This API is subject to change at any time. func (ca CertificateAuthenticator) Certificate(req AuthCertRequest) (*tls.Certificate, error) { return ca.ClientCertificate, nil } // Credentials returns the credentials for a particular service. // VOLATILE: This API is subject to change at any time. func (ca CertificateAuthenticator) Credentials(req AuthCredsRequest) ([]UserPassPair, error) { return []UserPassPair{{ Username: "", Password: "", }}, nil } type coreAuthWrapper struct { auth Authenticator } func (auth *coreAuthWrapper) SupportsTLS() bool { return auth.auth.SupportsTLS() } func (auth *coreAuthWrapper) SupportsNonTLS() bool { return auth.auth.SupportsNonTLS() } func (auth *coreAuthWrapper) Certificate(req gocbcore.AuthCertRequest) (*tls.Certificate, error) { return auth.auth.Certificate(AuthCertRequest{ Service: ServiceType(req.Service), Endpoint: req.Endpoint, }) } func (auth *coreAuthWrapper) Credentials(req gocbcore.AuthCredsRequest) ([]gocbcore.UserPassPair, error) { creds, err := auth.auth.Credentials(AuthCredsRequest{ Service: ServiceType(req.Service), Endpoint: req.Endpoint, }) if err != nil { return nil, err } coreCreds := make([]gocbcore.UserPassPair, len(creds)) for credIdx, userPass := range creds { coreCreds[credIdx] = gocbcore.UserPassPair(userPass) } return coreCreds, nil } gocb-2.6.3/base_queryindexes.go000066400000000000000000000302531441755043100165110ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "regexp" "strconv" "strings" "time" ) type baseQueryIndexManager struct { provider queryIndexQueryProvider globalTimeout time.Duration tracer RequestTracer meter *meterWrapper } type queryIndexQueryProvider interface { Query(statement string, opts *QueryOptions) (*QueryResult, error) } func (qm *baseQueryIndexManager) tryParseErrorMessage(err error) error { var qErr *QueryError if !errors.As(err, &qErr) { return err } if len(qErr.Errors) == 0 { return err } firstErr := qErr.Errors[0] var innerErr error if firstErr.Code == 12016 { innerErr = ErrIndexNotFound } else if firstErr.Code == 4300 { innerErr = ErrIndexExists } else { // Older server versions don't return meaningful error codes when it comes to index management // so we need to go spelunking. msg := strings.ToLower(firstErr.Message) if match, err := regexp.MatchString(".*?ndex .*? not found.*", msg); err == nil && match { innerErr = ErrIndexNotFound } else if match, err := regexp.MatchString(".*?ndex .*? already exists.*", msg); err == nil && match { innerErr = ErrIndexExists } } if innerErr == nil { return err } return QueryError{ InnerError: innerErr, Statement: qErr.Statement, ClientContextID: qErr.ClientContextID, Errors: qErr.Errors, Endpoint: qErr.Endpoint, RetryReasons: qErr.RetryReasons, RetryAttempts: qErr.RetryAttempts, } } func (qm *baseQueryIndexManager) doQuery(q string, opts *QueryOptions) ([][]byte, error) { if opts.Timeout == 0 { opts.Timeout = qm.globalTimeout } result, err := qm.provider.Query(q, opts) if err != nil { return nil, qm.tryParseErrorMessage(err) } var rows [][]byte for result.Next() { var row json.RawMessage err := result.Row(&row) if err != nil { logWarnf("management operation failed to read row: %s", err) } else { rows = append(rows, row) } } err = result.Err() if err != nil { return nil, qm.tryParseErrorMessage(err) } return rows, nil } type jsonQueryIndex struct { Name string `json:"name"` IsPrimary bool `json:"is_primary"` Type QueryIndexType `json:"using"` State string `json:"state"` Keyspace string `json:"keyspace_id"` Namespace string `json:"namespace_id"` IndexKey []string `json:"index_key"` Condition string `json:"condition"` Partition string `json:"partition"` Scope string `json:"scope_id"` Bucket string `json:"bucket_id"` } // QueryIndex represents a Couchbase GSI index. type QueryIndex struct { Name string IsPrimary bool Type QueryIndexType State string Keyspace string Namespace string IndexKey []string Condition string Partition string CollectionName string ScopeName string BucketName string } func (index *QueryIndex) fromData(data jsonQueryIndex) error { index.Name = data.Name index.IsPrimary = data.IsPrimary index.Type = data.Type index.State = data.State index.Keyspace = data.Keyspace index.Namespace = data.Namespace index.IndexKey = data.IndexKey index.Condition = data.Condition index.Partition = data.Partition index.ScopeName = data.Scope if data.Bucket == "" { index.BucketName = data.Keyspace } else { index.BucketName = data.Bucket } if data.Scope != "" { index.CollectionName = data.Keyspace } return nil } type createQueryIndexOptions struct { IgnoreIfExists bool Deferred bool NumReplicas int Timeout time.Duration RetryStrategy RetryStrategy } func (qm *baseQueryIndexManager) CreateIndex( ctx context.Context, parent RequestSpan, keyspace, indexName string, fields []string, opts createQueryIndexOptions, ) error { var qs string spanName := "manager_query_create_index" if len(fields) == 0 { spanName = "manager_query_create_primary_index" qs += "CREATE PRIMARY INDEX" } else { qs += "CREATE INDEX" } if indexName != "" { qs += " `" + indexName + "`" } qs += " ON " + keyspace if len(fields) > 0 { qs += " (" for i := 0; i < len(fields); i++ { if i > 0 { qs += ", " } qs += "`" + fields[i] + "`" } qs += ")" } var with []string if opts.Deferred { with = append(with, `"defer_build":true`) } if opts.NumReplicas > 0 { with = append(with, `"num_replica":`+strconv.Itoa(opts.NumReplicas)) } if len(with) > 0 { withStr := strings.Join(with, ",") qs += " WITH {" + withStr + "}" } start := time.Now() defer qm.meter.ValueRecord(meterValueServiceManagement, spanName, start) span := createSpan(qm.tracer, parent, spanName, "management") defer span.End() _, err := qm.doQuery(qs, &QueryOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, Adhoc: true, ParentSpan: span, Context: ctx, }) if err == nil { return nil } if opts.IgnoreIfExists && errors.Is(err, ErrIndexExists) { return nil } return err } type dropQueryIndexOptions struct { IgnoreIfNotExists bool Timeout time.Duration RetryStrategy RetryStrategy UseCollectionsSyntax bool } func (qm *baseQueryIndexManager) DropIndex( ctx context.Context, parent RequestSpan, keyspace, indexName string, opts dropQueryIndexOptions, ) error { var qs string spanName := "manager_query_drop_index" if indexName == "" { spanName = "manager_query_drop_primary_index" qs += "DROP PRIMARY INDEX ON " + keyspace } else { if opts.UseCollectionsSyntax { qs += "DROP INDEX `" + indexName + "` ON " + keyspace } else { qs += "DROP INDEX " + keyspace + ".`" + indexName + "`" } } start := time.Now() defer qm.meter.ValueRecord(meterValueServiceManagement, spanName, start) span := createSpan(qm.tracer, parent, spanName, "management") defer span.End() _, err := qm.doQuery(qs, &QueryOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, Adhoc: true, ParentSpan: span, Context: ctx, }) if err == nil { return nil } if opts.IgnoreIfNotExists && errors.Is(err, ErrIndexNotFound) { return nil } return err } type getAllQueryIndexesOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } func (qm *baseQueryIndexManager) GetAllIndexes( ctx context.Context, parent RequestSpan, whereClause string, params map[string]interface{}, opts getAllQueryIndexesOptions, ) ([]QueryIndex, error) { start := time.Now() defer qm.meter.ValueRecord(meterValueServiceManagement, "manager_query_get_all_indexes", start) return qm.getAllIndexes(ctx, parent, whereClause, params, opts) } func (qm *baseQueryIndexManager) getAllIndexes( ctx context.Context, parent RequestSpan, whereClause string, params map[string]interface{}, opts getAllQueryIndexesOptions, ) ([]QueryIndex, error) { span := createSpan(qm.tracer, parent, "manager_query_get_all_indexes", "management") defer span.End() q := "SELECT `idx`.* FROM system:indexes AS idx WHERE " + whereClause + " AND `using` = \"gsi\" " + "ORDER BY is_primary DESC, name ASC" rows, err := qm.doQuery(q, &QueryOptions{ NamedParameters: params, Readonly: true, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, Adhoc: true, ParentSpan: span, Context: ctx, }) if err != nil { return nil, err } var indexes []QueryIndex for _, row := range rows { var jsonIdx jsonQueryIndex err := json.Unmarshal(row, &jsonIdx) if err != nil { return nil, err } var index QueryIndex err = index.fromData(jsonIdx) if err != nil { return nil, err } indexes = append(indexes, index) } return indexes, nil } type buildDeferredQueryIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } func (qm *baseQueryIndexManager) BuildDeferredIndexes( keyspace string, getIndexesWhereClause string, getIndexesWhereParams map[string]interface{}, opts buildDeferredQueryIndexOptions, ) ([]string, error) { start := time.Now() defer qm.meter.ValueRecord(meterValueServiceManagement, "manager_query_build_deferred_indexes", start) span := createSpan(qm.tracer, opts.ParentSpan, "manager_query_build_deferred_indexes", "management") defer span.End() query := "SELECT RAW name from system:indexes WHERE " + getIndexesWhereClause + " AND state = \"deferred\"" indexesRes, err := qm.doQuery(query, &QueryOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, Adhoc: true, ParentSpan: span, Context: opts.Context, NamedParameters: getIndexesWhereParams, }) if err != nil { return nil, err } var deferredList []string for _, row := range indexesRes { var name string err := json.Unmarshal(row, &name) if err != nil { return nil, err } deferredList = append(deferredList, name) } if len(deferredList) == 0 { // Don't try to build an empty index list return nil, nil } var qs string qs += "BUILD INDEX ON " + keyspace + "(" for i := 0; i < len(deferredList); i++ { if i > 0 { qs += ", " } qs += "`" + deferredList[i] + "`" } qs += ")" _, err = qm.doQuery(qs, &QueryOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, Adhoc: true, ParentSpan: span, Context: opts.Context, }) if err != nil { return nil, err } return deferredList, nil } func checkIndexesActive(indexes []QueryIndex, checkList []string) (bool, error) { var checkIndexes []QueryIndex for i := 0; i < len(checkList); i++ { indexName := checkList[i] for j := 0; j < len(indexes); j++ { if indexes[j].Name == indexName { checkIndexes = append(checkIndexes, indexes[j]) break } } } if len(checkIndexes) != len(checkList) { return false, ErrIndexNotFound } for i := 0; i < len(checkIndexes); i++ { if checkIndexes[i].State != "online" { logDebugf("Index not online: %s is in state %s", checkIndexes[i].Name, checkIndexes[i].State) return false, nil } } return true, nil } type watchQueryIndexOptions struct { WatchPrimary bool RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } func (qm *baseQueryIndexManager) WatchIndexes( getAllWhereClause string, getAllWhereParams map[string]interface{}, watchList []string, timeout time.Duration, opts watchQueryIndexOptions, ) error { start := time.Now() defer qm.meter.ValueRecord(meterValueServiceManagement, "manager_query_watch_indexes", start) span := createSpan(qm.tracer, opts.ParentSpan, "manager_query_watch_indexes", "management") defer span.End() if opts.WatchPrimary { watchList = append(watchList, "#primary") } deadline := time.Now().Add(timeout) curInterval := 50 * time.Millisecond for { if deadline.Before(time.Now()) { return ErrUnambiguousTimeout } indexes, err := qm.getAllIndexes( opts.Context, span, getAllWhereClause, getAllWhereParams, getAllQueryIndexesOptions{ Timeout: time.Until(deadline), RetryStrategy: opts.RetryStrategy, }) if err != nil { return err } allOnline, err := checkIndexesActive(indexes, watchList) if err != nil { return err } if allOnline { break } curInterval += 500 * time.Millisecond if curInterval > 1000 { curInterval = 1000 } // Make sure we don't sleep past our overall deadline, if we adjust the // deadline then it will be caught at the top of this loop as a timeout. sleepDeadline := time.Now().Add(curInterval) if sleepDeadline.After(deadline) { sleepDeadline = deadline } // wait till our next poll interval time.Sleep(time.Until(sleepDeadline)) } return nil } gocb-2.6.3/bucket.go000066400000000000000000000113631441755043100142500ustar00rootroot00000000000000package gocb import ( "time" "github.com/couchbase/gocbcore/v10" ) // Bucket represents a single bucket within a cluster. type Bucket struct { bucketName string timeoutsConfig TimeoutsConfig transcoder Transcoder retryStrategyWrapper *retryStrategyWrapper tracer RequestTracer meter *meterWrapper useServerDurations bool useMutationTokens bool bootstrapError error connectionManager connectionManager getTransactions func() *Transactions } func newBucket(c *Cluster, bucketName string) *Bucket { return &Bucket{ bucketName: bucketName, timeoutsConfig: c.timeoutsConfig, transcoder: c.transcoder, retryStrategyWrapper: c.retryStrategyWrapper, tracer: c.tracer, meter: c.meter, useServerDurations: c.useServerDurations, useMutationTokens: c.useMutationTokens, connectionManager: c.connectionManager, getTransactions: c.Transactions, } } func (b *Bucket) setBootstrapError(err error) { b.bootstrapError = err } func (b *Bucket) getKvProvider() (kvProvider, error) { if b.bootstrapError != nil { return nil, b.bootstrapError } agent, err := b.connectionManager.getKvProvider(b.bucketName) if err != nil { return nil, err } return agent, nil } func (b *Bucket) getKvCapabilitiesProvider() (kvCapabilityVerifier, error) { if b.bootstrapError != nil { return nil, b.bootstrapError } agent, err := b.connectionManager.getKvCapabilitiesProvider(b.bucketName) if err != nil { return nil, err } return agent, nil } func (b *Bucket) getQueryProvider() (queryProvider, error) { if b.bootstrapError != nil { return nil, b.bootstrapError } agent, err := b.connectionManager.getQueryProvider() if err != nil { return nil, err } return agent, nil } func (b *Bucket) getAnalyticsProvider() (analyticsProvider, error) { if b.bootstrapError != nil { return nil, b.bootstrapError } agent, err := b.connectionManager.getAnalyticsProvider() if err != nil { return nil, err } return agent, nil } // Name returns the name of the bucket. func (b *Bucket) Name() string { return b.bucketName } // Scope returns an instance of a Scope. func (b *Bucket) Scope(scopeName string) *Scope { return newScope(b, scopeName) } // DefaultScope returns an instance of the default scope. func (b *Bucket) DefaultScope() *Scope { return b.Scope("_default") } // Collection returns an instance of a collection from within the default scope. func (b *Bucket) Collection(collectionName string) *Collection { return b.DefaultScope().Collection(collectionName) } // DefaultCollection returns an instance of the default collection. func (b *Bucket) DefaultCollection() *Collection { return b.DefaultScope().Collection("_default") } // ViewIndexes returns a ViewIndexManager instance for managing views. func (b *Bucket) ViewIndexes() *ViewIndexManager { return &ViewIndexManager{ mgmtProvider: b, bucketName: b.Name(), tracer: b.tracer, meter: b.meter, } } // Collections provides functions for managing collections. func (b *Bucket) Collections() *CollectionManager { // TODO: return error for unsupported collections return &CollectionManager{ mgmtProvider: b, bucketName: b.Name(), tracer: b.tracer, meter: b.meter, } } // WaitUntilReady will wait for the bucket object to be ready for use. // At present this will wait until memd connections have been established with the server and are ready // to be used before performing a ping against the specified services (except KeyValue) which also // exist in the cluster map. // If no services are specified then will wait until KeyValue is ready. // Valid service types are: ServiceTypeKeyValue, ServiceTypeManagement, ServiceTypeQuery, ServiceTypeSearch, // ServiceTypeAnalytics, ServiceTypeViews. func (b *Bucket) WaitUntilReady(timeout time.Duration, opts *WaitUntilReadyOptions) error { if opts == nil { opts = &WaitUntilReadyOptions{} } if b.bootstrapError != nil { return b.bootstrapError } provider, err := b.connectionManager.getWaitUntilReadyProvider(b.bucketName) if err != nil { return err } desiredState := opts.DesiredState if desiredState == 0 { desiredState = ClusterStateOnline } services := opts.ServiceTypes gocbcoreServices := make([]gocbcore.ServiceType, len(services)) for i, svc := range services { gocbcoreServices[i] = gocbcore.ServiceType(svc) } wrapper := b.retryStrategyWrapper if opts.RetryStrategy != nil { wrapper = newRetryStrategyWrapper(opts.RetryStrategy) } err = provider.WaitUntilReady( opts.Context, time.Now().Add(timeout), gocbcore.WaitUntilReadyOptions{ DesiredState: gocbcore.ClusterState(desiredState), ServiceTypes: gocbcoreServices, RetryStrategy: wrapper, }, ) if err != nil { return maybeEnhanceCoreErr(err) } return nil } gocb-2.6.3/bucket_collectionsmgr.go000066400000000000000000000327641441755043100173640ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "net/url" "strings" "time" "github.com/google/uuid" "github.com/couchbase/gocbcore/v10" ) // CollectionSpec describes the specification of a collection. type CollectionSpec struct { Name string ScopeName string MaxExpiry time.Duration } // ScopeSpec describes the specification of a scope. type ScopeSpec struct { Name string Collections []CollectionSpec } // These 3 types are temporary. They are necessary for now as the server beta was released with ns_server returning // a different jsonManifest format to what it will return in the future. type jsonManifest struct { UID uint64 `json:"uid"` Scopes map[string]jsonManifestScope `json:"scopes"` } type jsonManifestScope struct { UID uint32 `json:"uid"` Collections map[string]jsonManifestCollection `json:"collections"` } type jsonManifestCollection struct { UID uint32 `json:"uid"` } // CollectionManager provides methods for performing collections management. type CollectionManager struct { mgmtProvider mgmtProvider bucketName string tracer RequestTracer meter *meterWrapper } func (cm *CollectionManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("failed to read http body: %s", err) return nil } errText := strings.ToLower(string(b)) if err := checkForRateLimitError(resp.StatusCode, errText); err != nil { return makeGenericMgmtError(err, req, resp, string(b)) } if strings.Contains(errText, "not found") && strings.Contains(errText, "collection") { return makeGenericMgmtError(ErrCollectionNotFound, req, resp, string(b)) } else if strings.Contains(errText, "not found") && strings.Contains(errText, "scope") { return makeGenericMgmtError(ErrScopeNotFound, req, resp, string(b)) } if strings.Contains(errText, "already exists") && strings.Contains(errText, "collection") { return makeGenericMgmtError(ErrCollectionExists, req, resp, string(b)) } else if strings.Contains(errText, "already exists") && strings.Contains(errText, "scope") { return makeGenericMgmtError(ErrScopeExists, req, resp, string(b)) } return makeGenericMgmtError(errors.New(errText), req, resp, string(b)) } // GetAllScopesOptions is the set of options available to the GetAllScopes operation. type GetAllScopesOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllScopes gets all scopes from the bucket. func (cm *CollectionManager) GetAllScopes(opts *GetAllScopesOptions) ([]ScopeSpec, error) { if opts == nil { opts = &GetAllScopesOptions{} } start := time.Now() defer cm.meter.ValueRecord(meterValueServiceManagement, "manager_collections_get_all_scopes", start) path := fmt.Sprintf("/pools/default/buckets/%s/scopes", cm.bucketName) span := createSpan(cm.tracer, opts.ParentSpan, "manager_collections_get_all_scopes", "management") span.SetAttribute("db.name", cm.bucketName) span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "GET", RetryStrategy: opts.RetryStrategy, IsIdempotent: true, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := cm.mgmtProvider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeMgmtBadStatusError("failed to get all scopes", &req, resp) } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { colErr := cm.tryParseErrorMessage(&req, resp) if colErr != nil { return nil, colErr } return nil, makeMgmtBadStatusError("failed to get all scopes", &req, resp) } var scopes []ScopeSpec var mfest gocbcore.Manifest jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&mfest) if err == nil { for _, scope := range mfest.Scopes { var collections []CollectionSpec for _, col := range scope.Collections { collections = append(collections, CollectionSpec{ Name: col.Name, ScopeName: scope.Name, MaxExpiry: time.Duration(col.MaxTTL) * time.Second, }) } scopes = append(scopes, ScopeSpec{ Name: scope.Name, Collections: collections, }) } } else { // Temporary support for older server version var oldMfest jsonManifest jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&oldMfest) if err != nil { return nil, err } for scopeName, scope := range oldMfest.Scopes { var collections []CollectionSpec for colName := range scope.Collections { collections = append(collections, CollectionSpec{ Name: colName, ScopeName: scopeName, }) } scopes = append(scopes, ScopeSpec{ Name: scopeName, Collections: collections, }) } } return scopes, nil } // CreateCollectionOptions is the set of options available to the CreateCollection operation. type CreateCollectionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateCollection creates a new collection on the bucket. func (cm *CollectionManager) CreateCollection(spec CollectionSpec, opts *CreateCollectionOptions) error { if spec.Name == "" { return makeInvalidArgumentsError("collection name cannot be empty") } if spec.ScopeName == "" { return makeInvalidArgumentsError("scope name cannot be empty") } if opts == nil { opts = &CreateCollectionOptions{} } start := time.Now() defer cm.meter.ValueRecord(meterValueServiceManagement, "manager_collections_create_collection", start) path := fmt.Sprintf("/pools/default/buckets/%s/scopes/%s/collections", cm.bucketName, spec.ScopeName) span := createSpan(cm.tracer, opts.ParentSpan, "manager_collections_create_collection", "management") span.SetAttribute("db.name", cm.bucketName) span.SetAttribute("db.couchbase.scope", spec.ScopeName) span.SetAttribute("db.couchbase.collection", spec.Name) span.SetAttribute("db.operation", "POST "+path) defer span.End() posts := url.Values{} posts.Add("name", spec.Name) if spec.MaxExpiry > 0 { posts.Add("maxTTL", fmt.Sprintf("%d", int(spec.MaxExpiry.Seconds()))) } eSpan := createSpan(cm.tracer, span, "request_encoding", "") encoded := posts.Encode() eSpan.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "POST", Body: []byte(encoded), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := cm.mgmtProvider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { colErr := cm.tryParseErrorMessage(&req, resp) if colErr != nil { return colErr } return makeMgmtBadStatusError("failed to create collection", &req, resp) } err = resp.Body.Close() if err != nil { logDebugf("Failed to close socket (%s)", err) } return nil } // DropCollectionOptions is the set of options available to the DropCollection operation. type DropCollectionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropCollection removes a collection. func (cm *CollectionManager) DropCollection(spec CollectionSpec, opts *DropCollectionOptions) error { if spec.Name == "" { return makeInvalidArgumentsError("collection name cannot be empty") } if spec.ScopeName == "" { return makeInvalidArgumentsError("scope name cannot be empty") } if opts == nil { opts = &DropCollectionOptions{} } start := time.Now() defer cm.meter.ValueRecord(meterValueServiceManagement, "manager_collections_drop_collection", start) path := fmt.Sprintf("/pools/default/buckets/%s/scopes/%s/collections/%s", cm.bucketName, spec.ScopeName, spec.Name) span := createSpan(cm.tracer, opts.ParentSpan, "manager_collections_drop_collection", "management") span.SetAttribute("db.name", cm.bucketName) span.SetAttribute("db.couchbase.scope", spec.ScopeName) span.SetAttribute("db.couchbase.collection", spec.Name) span.SetAttribute("db.operation", "DELETE "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "DELETE", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := cm.mgmtProvider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { colErr := cm.tryParseErrorMessage(&req, resp) if colErr != nil { return colErr } return makeMgmtBadStatusError("failed to drop collection", &req, resp) } err = resp.Body.Close() if err != nil { logDebugf("Failed to close socket (%s)", err) } return nil } // CreateScopeOptions is the set of options available to the CreateScope operation. type CreateScopeOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateScope creates a new scope on the bucket. func (cm *CollectionManager) CreateScope(scopeName string, opts *CreateScopeOptions) error { if scopeName == "" { return makeInvalidArgumentsError("scope name cannot be empty") } if opts == nil { opts = &CreateScopeOptions{} } start := time.Now() defer cm.meter.ValueRecord(meterValueServiceManagement, "manager_collections_create_scope", start) path := fmt.Sprintf("/pools/default/buckets/%s/scopes", cm.bucketName) span := createSpan(cm.tracer, opts.ParentSpan, "manager_collections_create_scope", "management") span.SetAttribute("db.name", cm.bucketName) span.SetAttribute("db.couchbase.scope", scopeName) span.SetAttribute("db.operation", "POST "+path) defer span.End() posts := url.Values{} posts.Add("name", scopeName) eSpan := createSpan(cm.tracer, span, "request_encoding", "") encoded := posts.Encode() eSpan.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "POST", Body: []byte(encoded), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := cm.mgmtProvider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { colErr := cm.tryParseErrorMessage(&req, resp) if colErr != nil { return colErr } return makeMgmtBadStatusError("failed to create scope", &req, resp) } err = resp.Body.Close() if err != nil { logDebugf("Failed to close socket (%s)", err) } return nil } // DropScopeOptions is the set of options available to the DropScope operation. type DropScopeOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropScope removes a scope. func (cm *CollectionManager) DropScope(scopeName string, opts *DropScopeOptions) error { if opts == nil { opts = &DropScopeOptions{} } start := time.Now() defer cm.meter.ValueRecord(meterValueServiceManagement, "manager_collections_drop_scope", start) path := fmt.Sprintf("/pools/default/buckets/%s/scopes/%s", cm.bucketName, scopeName) span := createSpan(cm.tracer, opts.ParentSpan, "manager_collections_drop_scope", "management") span.SetAttribute("db.name", cm.bucketName) span.SetAttribute("db.couchbase.scope", scopeName) span.SetAttribute("db.operation", "DELETE "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "DELETE", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := cm.mgmtProvider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { colErr := cm.tryParseErrorMessage(&req, resp) if colErr != nil { return colErr } return makeMgmtBadStatusError("failed to drop scope", &req, resp) } err = resp.Body.Close() if err != nil { logDebugf("Failed to close socket (%s)", err) } return nil } gocb-2.6.3/bucket_collectionsmgr_test.go000066400000000000000000000420211441755043100204060ustar00rootroot00000000000000package gocb import ( "errors" "fmt" "github.com/stretchr/testify/mock" "strconv" "time" ) func (suite *IntegrationTestSuite) TestCollectionManagerCrud() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) scopeName := generateDocId("testScope") collectionName := generateDocId("testCollection") mgr := globalBucket.Collections() err := mgr.CreateScope(scopeName, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateScope(scopeName, nil) if !errors.Is(err, ErrScopeExists) { suite.T().Fatalf("Expected create scope to error with ScopeExists but was %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: collectionName, ScopeName: scopeName, MaxExpiry: 5 * time.Second, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: collectionName, ScopeName: scopeName, }, nil) if !errors.Is(err, ErrCollectionExists) { suite.T().Fatalf("Expected create collection to error with CollectionExists but was %v", err) } scopes, err := mgr.GetAllScopes(nil) if err != nil { suite.T().Fatalf("Failed to GetAllScopes %v", err) } if len(scopes) < 2 { suite.T().Fatalf("Expected scopes to contain at least 2 scopes but was %v", scopes) } var found bool for _, scope := range scopes { if scope.Name != scopeName { continue } found = true if suite.Assert().Len(scope.Collections, 1) { col := scope.Collections[0] suite.Assert().Equal(collectionName, col.Name) suite.Assert().Equal(scopeName, col.ScopeName) suite.Assert().Equal(5*time.Second, col.MaxExpiry) } break } suite.Assert().True(found) err = mgr.DropCollection(CollectionSpec{ Name: collectionName, ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Expected DropCollection to not error but was %v", err) } err = mgr.DropCollection(CollectionSpec{ Name: collectionName, ScopeName: scopeName, }, nil) if !errors.Is(err, ErrCollectionNotFound) { suite.T().Fatalf("Expected drop collection to error with ErrCollectionNotFound but was %v", err) } err = mgr.DropScope(scopeName, nil) if err != nil { suite.T().Fatalf("Expected DropScope to not error but was %v", err) } err = mgr.DropScope(scopeName, nil) if !errors.Is(err, ErrScopeNotFound) { suite.T().Fatalf("Expected drop scope to error with ErrScopeNotFound but was %v", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(9, len(nilParents)) suite.AssertHTTPOpSpan(nilParents[0], "manager_collections_create_scope", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, scope: scopeName, service: "management", operationID: "POST " + fmt.Sprintf("/pools/default/buckets/%s/scopes", globalConfig.Bucket), numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", }) suite.AssertHTTPOpSpan(nilParents[2], "manager_collections_create_collection", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, scope: scopeName, collection: collectionName, service: "management", operationID: "POST " + fmt.Sprintf("/pools/default/buckets/%s/scopes/%s/collections", globalConfig.Bucket, scopeName), numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", }) suite.AssertHTTPOpSpan(nilParents[4], "manager_collections_get_all_scopes", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, service: "management", operationID: "GET " + fmt.Sprintf("/pools/default/buckets/%s/scopes", globalConfig.Bucket), numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", }) suite.AssertHTTPOpSpan(nilParents[5], "manager_collections_drop_collection", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, scope: scopeName, collection: collectionName, service: "management", operationID: "DELETE " + fmt.Sprintf("/pools/default/buckets/%s/scopes/%s/collections/%s", globalConfig.Bucket, scopeName, collectionName), numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", }) suite.AssertHTTPOpSpan(nilParents[7], "manager_collections_drop_scope", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, scope: scopeName, service: "management", operationID: "DELETE " + fmt.Sprintf("/pools/default/buckets/%s/scopes/%s", globalConfig.Bucket, scopeName), numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_create_scope"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_create_collection"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_get_all_scopes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_drop_scope"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_drop_collection"), 2, false) } func (suite *IntegrationTestSuite) TestDropNonExistentScope() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) scopeName := generateDocId("testDropScopeX") mgr := globalBucket.Collections() err := mgr.CreateScope(scopeName, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: generateDocId("testDropCollection"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.DropScope(generateDocId("testScopeX"), nil) if err == nil { suite.T().Fatalf("Expected error to be non-nil") } } func (suite *IntegrationTestSuite) TestDropNonExistentCollection() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) scopeName := generateDocId("testDropScopeY") mgr := globalBucket.Collections() err := mgr.CreateScope(scopeName, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: generateDocId("testDropCollectionY"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.DropCollection(CollectionSpec{ Name: generateDocId("testCollectionZ"), ScopeName: scopeName, }, nil) if err == nil { suite.T().Fatalf("Expected error to be non-nil") } } func (suite *IntegrationTestSuite) TestCollectionsAreNotPresent() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) scopeName1 := generateDocId("scope-1-") scopeName2 := generateDocId("scope-2-") collectionName1 := generateDocId("collection-1-") collectionName2 := generateDocId("collection-2-") mgr := globalBucket.Collections() err := mgr.CreateScope(scopeName1, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateScope(scopeName2, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: collectionName1, ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: collectionName2, ScopeName: scopeName2, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.DropCollection(CollectionSpec{ Name: collectionName1, ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Expected DropCollection to not error but was %v", err) } err = mgr.DropCollection(CollectionSpec{ Name: collectionName2, ScopeName: scopeName2, }, nil) if err != nil { suite.T().Fatalf("Expected DropCollection to not error but was %v", err) } err = mgr.DropCollection(CollectionSpec{ Name: collectionName2, ScopeName: scopeName2, }, nil) if err == nil { suite.T().Fatalf("Expected error to be non-nil") } } func (suite *IntegrationTestSuite) TestDropScopesAreNotExist() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) mgr := globalBucket.Collections() err := mgr.CreateScope("testDropScope1", nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateScope("testDropScope2", nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: "testDropCollection1", ScopeName: "testDropScope1", }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.CreateCollection(CollectionSpec{ Name: "testDropCollection2", ScopeName: "testDropScope2", }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = mgr.DropScope("testDropScope1", nil) if err != nil { suite.T().Fatalf("Expected DropScope to not error but was %v", err) } err = mgr.DropScope("testDropScope2", nil) if err != nil { suite.T().Fatalf("Expected DropScope to not error but was %v", err) } err = mgr.DropScope("testDropScope1", nil) if err == nil { suite.T().Fatalf("Expected error to be non-nil") } err = mgr.DropCollection(CollectionSpec{ Name: "testDropCollection1", ScopeName: "testDropScope1", }, nil) if err == nil { suite.T().Fatalf("Expected error to be non-nil") } } func (suite *IntegrationTestSuite) TestGetAllScopes() { suite.skipIfUnsupported(CollectionsFeature) bucket1 := globalBucket.Collections() err := bucket1.CreateScope(generateDocId("testScopeX1"), nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucket1.CreateScope(generateDocId("testScopeX2"), nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucket1.CreateScope(generateDocId("testScopeX3"), nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucket1.CreateScope(generateDocId("testScopeX4"), nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucket1.CreateScope(generateDocId("testScopeX5"), nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } scopes, err := bucket1.GetAllScopes(nil) if err != nil { suite.T().Fatalf("Failed to GetAllScopes %v", err) } if len(scopes) < 5 { suite.T().Fatalf("Expected scopes to contain total of 5 scopes but was %v", scopes) } } func (suite *IntegrationTestSuite) TestCollectionsInBucket() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) scopeName := generateDocId("collectionsInBucketScope") bucket1 := globalBucket.Collections() err := bucket1.CreateScope(scopeName, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucket1.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection1-"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucket1.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection2-"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucket1.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection3-"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucket1.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection4-"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucket1.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection5-"), ScopeName: scopeName, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } success := suite.tryUntil(time.Now().Add(5*time.Second), 500*time.Millisecond, func() bool { scopes, err := bucket1.GetAllScopes(nil) if err != nil { suite.T().Fatalf("Failed to GetAllScopes %v", err) } var scope *ScopeSpec for i, s := range scopes { if s.Name == scopeName { scope = &scopes[i] } } suite.Require().NotNil(scope) if len(scope.Collections) != 5 { suite.T().Logf("Expected collections in scope should be 5 but was %v", scope) return false } return true }) suite.Require().True(success) } func (suite *IntegrationTestSuite) TestNumberOfCollectionInScope() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) scopeName1 := generateDocId("numCollectionsScope1-") scopeName2 := generateDocId("numCollectionsScope2-") bucketX := globalBucket.Collections() err := bucketX.CreateScope(scopeName1, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucketX.CreateScope(scopeName2, nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection1-"), ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection2-"), ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection3-"), ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection4-"), ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection5-"), ScopeName: scopeName1, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection6-"), ScopeName: scopeName2, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } err = bucketX.CreateCollection(CollectionSpec{ Name: generateDocId("testCollection6-"), ScopeName: scopeName2, }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } scopes, err := bucketX.GetAllScopes(nil) if err != nil { suite.T().Fatalf("Failed to GetAllScopes %v", err) } var scope *ScopeSpec for i, s := range scopes { if s.Name == scopeName1 { scope = &scopes[i] } } suite.Require().NotNil(scope) if len(scope.Collections) != 5 { suite.T().Fatalf("Expected collections in scope should be 5 but was %v", scope) } } func (suite *IntegrationTestSuite) TestMaxNumberOfCollectionInScope() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(CollectionsManagerFeature) suite.skipIfUnsupported(CollectionsManagerMaxCollectionsFeature) testBucket1 := globalBucket.Collections() err := testBucket1.CreateScope("singleScope", nil) if err != nil { suite.T().Fatalf("Failed to create scope %v", err) } for i := 0; i < 1000; i++ { err = testBucket1.CreateCollection(CollectionSpec{ Name: strconv.Itoa(1000 + i), ScopeName: "singleScope", }, nil) if err != nil { suite.T().Fatalf("Failed to create collection %v", err) } } success := suite.tryUntil(time.Now().Add(15*time.Second), 100*time.Millisecond, func() bool { scopes, err := testBucket1.GetAllScopes(nil) if err != nil { suite.T().Logf("Failed to GetAllScopes %v", err) return false } var scope *ScopeSpec for i, s := range scopes { if s.Name == "singleScope" { scope = &scopes[i] } } if scope == nil { suite.T().Log("scope not found") return false } if len(scope.Collections) != 1000 { suite.T().Logf("Expected collections in scope should be 1000 but was %v", len(scope.Collections)) return false } return true }) suite.Require().True(success) } func (suite *UnitTestSuite) TestGetAllScopesMgmtRequestFails() { provider := new(mockMgmtProvider) provider.On("executeMgmtRequest", nil, mock.AnythingOfType("gocb.mgmtRequest")).Return(nil, errors.New("http send failure")) mgr := CollectionManager{ mgmtProvider: provider, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}, isNoopMeter: true}, } scopes, err := mgr.GetAllScopes(nil) suite.Require().NotNil(err) suite.Require().Nil(scopes) } gocb-2.6.3/bucket_internal.go000066400000000000000000000030751441755043100161450ustar00rootroot00000000000000package gocb import "github.com/couchbase/gocbcore/v10" type kvCapabilityVerifier interface { BucketCapabilityStatus(cap gocbcore.BucketCapability) gocbcore.BucketCapabilityStatus } // InternalBucket is used for internal functionality. // Internal: This should never be used and is not supported. type InternalBucket struct { bucket *Bucket } // Internal returns an InternalBucket. // Internal: This should never be used and is not supported. func (b *Bucket) Internal() *InternalBucket { return &InternalBucket{bucket: b} } // IORouter returns the collection's internal core router. func (ib *InternalBucket) IORouter() (*gocbcore.Agent, error) { return ib.bucket.connectionManager.connection(ib.bucket.Name()) } // HasCapabilityStatus verifies whether support for a server capability is in a given state. func (ib *InternalBucket) CapabilityStatus(cap Capability) (CapabilityStatus, error) { switch cap { case CapabilityCreateAsDeleted: return ib.bucketCapabilityStatus(gocbcore.BucketCapabilityCreateAsDeleted) case CapabilityDurableWrites: return ib.bucketCapabilityStatus(gocbcore.BucketCapabilityDurableWrites) case CapabilityReplaceBodyWithXattr: return ib.bucketCapabilityStatus(gocbcore.BucketCapabilityReplaceBodyWithXattr) default: return CapabilityStatusUnsupported, nil } } func (ib *InternalBucket) bucketCapabilityStatus(capability gocbcore.BucketCapability) (CapabilityStatus, error) { provider, err := ib.bucket.getKvCapabilitiesProvider() if err != nil { return 0, err } return CapabilityStatus(provider.BucketCapabilityStatus(capability)), nil } gocb-2.6.3/bucket_internal_test.go000066400000000000000000000023611441755043100172010ustar00rootroot00000000000000package gocb import "github.com/couchbase/gocbcore/v10" func (suite *UnitTestSuite) TestCollectionInternal_BucketCapabilityStatus_Supported() { provider := new(mockKvCapabilityVerifier) provider.On( "BucketCapabilityStatus", gocbcore.BucketCapabilityDurableWrites, ).Return(gocbcore.BucketCapabilityStatusSupported) cli := new(mockConnectionManager) cli.On("getKvCapabilitiesProvider", "mock").Return(provider, nil) b := suite.bucket("mock", suite.defaultTimeoutConfig(), cli) ib := b.Internal() status, err := ib.CapabilityStatus(CapabilityDurableWrites) suite.Require().Nil(err) suite.Assert().Equal(CapabilityStatusSupported, status) } func (suite *UnitTestSuite) TestCollectionInternal_BucketCapabilityStatus_Unsupported() { provider := new(mockKvCapabilityVerifier) provider.On( "BucketCapabilityStatus", gocbcore.BucketCapabilityDurableWrites, ).Return(gocbcore.BucketCapabilityStatusUnsupported) cli := new(mockConnectionManager) cli.On("getKvCapabilitiesProvider", "mock").Return(provider, nil) b := suite.bucket("mock", suite.defaultTimeoutConfig(), cli) ib := b.Internal() status, err := ib.CapabilityStatus(CapabilityDurableWrites) suite.Require().Nil(err) suite.Assert().Equal(CapabilityStatusUnsupported, status) } gocb-2.6.3/bucket_ping.go000066400000000000000000000060261441755043100152650ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "time" ) // EndpointPingReport represents a single entry in a ping report. type EndpointPingReport struct { ID string Local string Remote string State PingState Error string Namespace string Latency time.Duration } // PingResult encapsulates the details from a executed ping operation. type PingResult struct { ID string Services map[ServiceType][]EndpointPingReport sdk string } type jsonEndpointPingReport struct { ID string `json:"id,omitempty"` Local string `json:"local,omitempty"` Remote string `json:"remote,omitempty"` State string `json:"state,omitempty"` Error string `json:"error,omitempty"` Namespace string `json:"namespace,omitempty"` LatencyUs uint64 `json:"latency_us"` } type jsonPingReport struct { Version uint16 `json:"version"` SDK string `json:"sdk,omitempty"` ID string `json:"id,omitempty"` Services map[string][]jsonEndpointPingReport `json:"services,omitempty"` } // MarshalJSON generates a JSON representation of this ping report. func (report *PingResult) MarshalJSON() ([]byte, error) { jsonReport := jsonPingReport{ Version: 2, SDK: report.sdk, ID: report.ID, Services: make(map[string][]jsonEndpointPingReport), } for serviceType, serviceInfo := range report.Services { serviceStr := serviceTypeToString(serviceType) if _, ok := jsonReport.Services[serviceStr]; !ok { jsonReport.Services[serviceStr] = make([]jsonEndpointPingReport, 0) } for _, service := range serviceInfo { jsonReport.Services[serviceStr] = append(jsonReport.Services[serviceStr], jsonEndpointPingReport{ ID: service.ID, Local: service.Local, Remote: service.Remote, State: pingStateToString(service.State), Error: service.Error, Namespace: service.Namespace, LatencyUs: uint64(service.Latency / time.Nanosecond), }) } } return json.Marshal(&jsonReport) } // PingOptions are the options available to the Ping operation. type PingOptions struct { ServiceTypes []ServiceType ReportID string Timeout time.Duration ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // Ping will ping a list of services and verify they are active and // responding in an acceptable period of time. func (b *Bucket) Ping(opts *PingOptions) (*PingResult, error) { if opts == nil { opts = &PingOptions{} } span := createSpan(b.tracer, opts.ParentSpan, "ping", "kv") defer span.End() startTime := time.Now() defer b.meter.ValueRecord(meterValueServiceKV, "ping", startTime) provider, err := b.connectionManager.getDiagnosticsProvider(b.bucketName) if err != nil { return nil, err } return ping(opts.Context, provider, opts, b.timeoutsConfig, span) } gocb-2.6.3/bucket_ping_test.go000066400000000000000000000067031441755043100163260ustar00rootroot00000000000000package gocb import ( "errors" "time" "github.com/stretchr/testify/mock" gocbcore "github.com/couchbase/gocbcore/v10" ) func (suite *UnitTestSuite) TestPingAll() { expectedResults := map[gocbcore.ServiceType][]gocbcore.EndpointPingResult{ gocbcore.MemdService: { { Endpoint: "server1", Latency: 25 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, { Endpoint: "server2", Latency: 42 * time.Millisecond, Error: errors.New("something"), Scope: "default", State: gocbcore.PingStateError, }, { Endpoint: "server3", Latency: 100 * time.Millisecond, Error: gocbcore.ErrUnambiguousTimeout, Scope: "default", State: gocbcore.PingStateTimeout, }, }, gocbcore.N1qlService: { { Endpoint: "server1", Latency: 50 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, { Endpoint: "server2", Latency: 34 * time.Millisecond, Error: errors.New("something"), Scope: "default", State: gocbcore.PingStateError, }, }, gocbcore.CbasService: { { Endpoint: "server1", Latency: 50 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, }, gocbcore.FtsService: { { Endpoint: "server3", Latency: 20 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, }, gocbcore.CapiService: { { Endpoint: "server2", Latency: 30 * time.Millisecond, Error: gocbcore.ErrUnambiguousTimeout, Scope: "default", State: gocbcore.PingStateTimeout, }, }, } pingResult := &gocbcore.PingResult{ ConfigRev: 64, Services: expectedResults, } pingProvider := new(mockDiagnosticsProvider) pingProvider. On("Ping", nil, mock.AnythingOfType("gocbcore.PingOptions")). Run(func(args mock.Arguments) { if len(args) != 2 { suite.T().Fatalf("Expected options to contain two arguments, was: %v", args) } opts := args.Get(1).(gocbcore.PingOptions) if len(opts.ServiceTypes) != 0 { suite.T().Errorf("Expected service types to be len 0 but was %v", opts.ServiceTypes) } }). Return(pingResult, nil) cli := new(mockConnectionManager) cli.On("getDiagnosticsProvider", "mock").Return(pingProvider, nil) b := suite.bucket("mock", suite.defaultTimeoutConfig(), cli) report, err := b.Ping(nil) if err != nil { suite.T().Fatalf("Expected ping to not return error but was %v", err) } if report.ID == "" { suite.T().Fatalf("Report ID was empty") } if len(report.Services) != 5 { suite.T().Fatalf("Expected services length to be 5 but was %d", len(report.Services)) } for serviceType, services := range report.Services { expectedServices, ok := expectedResults[gocbcore.ServiceType(serviceType)] if !ok { suite.T().Errorf("Unexpected service type in result: %v", serviceType) continue } for i, service := range services { expectedService := expectedServices[i] suite.Assert().Equal(expectedService.Latency, service.Latency) suite.Assert().Equal(expectedService.Scope, service.Namespace) if expectedService.Error == nil { suite.Assert().Empty(service.Error) } else { suite.Assert().Equal(expectedService.Error.Error(), service.Error) } suite.Assert().Equal(PingState(expectedService.State), service.State) suite.Assert().Equal(expectedService.Endpoint, service.Remote) suite.Assert().Equal(expectedService.ID, service.ID) } } } gocb-2.6.3/bucket_test.go000066400000000000000000000054371441755043100153140ustar00rootroot00000000000000package gocb import ( "errors" "time" ) func (suite *IntegrationTestSuite) TestBucketWaitUntilReady() { suite.skipIfUnsupported(WaitUntilReadyFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, }}) suite.Require().Nil(err, err) defer c.Close(nil) b := c.Bucket(globalConfig.Bucket) err = b.WaitUntilReady(globalCluster.waitUntilReadyTimeout(), nil) suite.Require().Nil(err, err) // Just test that we can use the bucket. _, err = b.DefaultCollection().Upsert(generateDocId("TestBucketWaitUntilReady"), "test", nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) TestBucketWaitUntilReadyInvalidAuth() { suite.skipIfUnsupported(WaitUntilReadyFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password + "nopethisshouldntwork", }}) suite.Require().Nil(err, err) defer c.Close(nil) b := c.Bucket(globalConfig.Bucket) start := time.Now() err = b.WaitUntilReady(globalCluster.waitUntilReadyTimeout(), nil) if !errors.Is(err, ErrUnambiguousTimeout) { suite.T().Fatalf("Expected unambiguous timeout error but was %v", err) } elapsed := time.Since(start) suite.Assert().GreaterOrEqual(int64(elapsed), int64(globalCluster.waitUntilReadyTimeout())) suite.Assert().LessOrEqual(int64(elapsed), int64(globalCluster.waitUntilReadyTimeout()+time.Second)) } func (suite *IntegrationTestSuite) TestBucketWaitUntilReadyFastFailAuth() { suite.skipIfUnsupported(WaitUntilReadyFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: "thisisaprettyunlikelypasswordtobeused", }}) suite.Require().Nil(err, err) defer c.Close(nil) b := c.Bucket(globalConfig.Bucket) err = b.WaitUntilReady(globalCluster.waitUntilReadyTimeout(), &WaitUntilReadyOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if !errors.Is(err, ErrAuthenticationFailure) { suite.T().Fatalf("Expected authentication error but was: %v", err) } } func (suite *IntegrationTestSuite) TestBucketWaitUntilReadyFastFailConnStr() { suite.skipIfUnsupported(WaitUntilReadyFeature) suite.skipIfUnsupported(WaitUntilReadyClusterFeature) c, err := Connect("10.10.10.10", ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, }}) suite.Require().Nil(err, err) defer c.Close(nil) b := c.Bucket(globalConfig.Bucket) err = b.WaitUntilReady(globalCluster.waitUntilReadyTimeout(), &WaitUntilReadyOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if !errors.Is(err, ErrTimeout) { suite.T().Fatalf("Expected timeout error but was: %v", err) } } gocb-2.6.3/bucket_viewindexes.go000066400000000000000000000356771441755043100167000ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "github.com/google/uuid" "io/ioutil" "strings" "time" ) // DesignDocumentNamespace represents which namespace a design document resides in. type DesignDocumentNamespace uint const ( // DesignDocumentNamespaceProduction means that a design document resides in the production namespace. DesignDocumentNamespaceProduction DesignDocumentNamespace = iota // DesignDocumentNamespaceDevelopment means that a design document resides in the development namespace. DesignDocumentNamespaceDevelopment ) // View represents a Couchbase view within a design document. type jsonView struct { Map string `json:"map,omitempty"` Reduce string `json:"reduce,omitempty"` } // DesignDocument represents a Couchbase design document containing multiple views. type jsonDesignDocument struct { Views map[string]jsonView `json:"views,omitempty"` } // View represents a Couchbase view within a design document. type View struct { Map string Reduce string } func (v *View) fromData(data jsonView) error { v.Map = data.Map v.Reduce = data.Reduce return nil } func (v *View) toData() (jsonView, error) { var data jsonView data.Map = v.Map data.Reduce = v.Reduce return data, nil } // DesignDocument represents a Couchbase design document containing multiple views. type DesignDocument struct { Name string Views map[string]View } func (dd *DesignDocument) fromData(data jsonDesignDocument, name string) error { dd.Name = name views := make(map[string]View) for viewName, viewData := range data.Views { var view View err := view.fromData(viewData) if err != nil { return err } views[viewName] = view } dd.Views = views return nil } func (dd *DesignDocument) toData() (jsonDesignDocument, string, error) { var data jsonDesignDocument views := make(map[string]jsonView) for viewName, view := range dd.Views { viewData, err := view.toData() if err != nil { return jsonDesignDocument{}, "", err } views[viewName] = viewData } data.Views = views return data, dd.Name, nil } // ViewIndexManager provides methods for performing View management. type ViewIndexManager struct { mgmtProvider mgmtProvider bucketName string tracer RequestTracer meter *meterWrapper } func (vm *ViewIndexManager) tryParseErrorMessage(req mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read view index manager response body: %s", err) return nil } if resp.StatusCode == 404 { if strings.Contains(strings.ToLower(string(b)), "not_found") { return makeGenericMgmtError(ErrDesignDocumentNotFound, &req, resp, string(b)) } return makeGenericMgmtError(errors.New(string(b)), &req, resp, string(b)) } if err := checkForRateLimitError(resp.StatusCode, string(b)); err != nil { return makeGenericMgmtError(err, &req, resp, string(b)) } var mgrErr bucketMgrErrorResp err = json.Unmarshal(b, &mgrErr) if err != nil { logDebugf("Failed to unmarshal error body: %s", err) return makeGenericMgmtError(errors.New(string(b)), &req, resp, string(b)) } var bodyErr error var firstErr string for _, err := range mgrErr.Errors { firstErr = strings.ToLower(err) break } if strings.Contains(firstErr, "bucket with given name already exists") { bodyErr = ErrBucketExists } else { bodyErr = errors.New(firstErr) } return makeGenericMgmtError(bodyErr, &req, resp, string(b)) } func (vm *ViewIndexManager) doMgmtRequest(ctx context.Context, req mgmtRequest) (*mgmtResponse, error) { resp, err := vm.mgmtProvider.executeMgmtRequest(ctx, req) if err != nil { return nil, err } return resp, nil } // GetDesignDocumentOptions is the set of options available to the ViewIndexManager GetDesignDocument operation. type GetDesignDocumentOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } func (vm *ViewIndexManager) ddocName(name string, namespace DesignDocumentNamespace) string { if namespace == DesignDocumentNamespaceProduction { if strings.HasPrefix(name, "dev_") { name = strings.TrimLeft(name, "dev_") } } else { if !strings.HasPrefix(name, "dev_") { name = "dev_" + name } } return name } // GetDesignDocument retrieves a single design document for the given bucket. func (vm *ViewIndexManager) GetDesignDocument(name string, namespace DesignDocumentNamespace, opts *GetDesignDocumentOptions) (*DesignDocument, error) { if opts == nil { opts = &GetDesignDocumentOptions{} } start := time.Now() defer vm.meter.ValueRecord(meterValueServiceManagement, "manager_views_get_design_document", start) return vm.getDesignDocument(name, namespace, time.Now(), opts) } func (vm *ViewIndexManager) getDesignDocument(name string, namespace DesignDocumentNamespace, startTime time.Time, opts *GetDesignDocumentOptions) (*DesignDocument, error) { name = vm.ddocName(name, namespace) span := createSpan(vm.tracer, opts.ParentSpan, "manager_views_get_design_document", "management") span.SetAttribute("db.operation", "GET "+fmt.Sprintf("/_design/%s", name)) span.SetAttribute("db.name", vm.bucketName) defer span.End() req := mgmtRequest{ Service: ServiceTypeViews, Path: fmt.Sprintf("/_design/%s", name), Method: "GET", IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), UniqueID: uuid.New().String(), } resp, err := vm.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { vwErr := vm.tryParseErrorMessage(req, resp) if vwErr != nil { return nil, vwErr } return nil, makeMgmtBadStatusError("failed to get design document", &req, resp) } var ddocData jsonDesignDocument jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&ddocData) if err != nil { return nil, err } ddocName := strings.TrimPrefix(name, "dev_") var ddoc DesignDocument err = ddoc.fromData(ddocData, ddocName) if err != nil { return nil, err } return &ddoc, nil } // GetAllDesignDocumentsOptions is the set of options available to the ViewIndexManager GetAllDesignDocuments operation. type GetAllDesignDocumentsOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllDesignDocuments will retrieve all design documents for the given bucket. func (vm *ViewIndexManager) GetAllDesignDocuments(namespace DesignDocumentNamespace, opts *GetAllDesignDocumentsOptions) ([]DesignDocument, error) { if opts == nil { opts = &GetAllDesignDocumentsOptions{} } start := time.Now() defer vm.meter.ValueRecord(meterValueServiceManagement, "manager_views_get_all_design_documents", start) path := fmt.Sprintf("/pools/default/buckets/%s/ddocs", vm.bucketName) span := createSpan(vm.tracer, opts.ParentSpan, "manager_views_get_all_design_documents", "management") span.SetAttribute("db.operation", "GET "+path) span.SetAttribute("db.name", vm.bucketName) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "GET", IsIdempotent: true, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, parentSpanCtx: span.Context(), UniqueID: uuid.New().String(), } resp, err := vm.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { vwErr := vm.tryParseErrorMessage(req, resp) if vwErr != nil { return nil, vwErr } return nil, makeMgmtBadStatusError("failed to get design documents", &req, resp) } var ddocsResp struct { Rows []struct { Doc struct { Meta struct { ID string `json:"id"` } JSON jsonDesignDocument `json:"json"` } `json:"doc"` } `json:"rows"` } jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&ddocsResp) if err != nil { return nil, err } var ddocs []DesignDocument for _, ddocData := range ddocsResp.Rows { if len(ddocData.Doc.Meta.ID) <= 8 { logErrorf("Design document name was less than 9 characters long: %s", ddocData.Doc.Meta.ID) continue } ddocName := ddocData.Doc.Meta.ID[8:] isDevDoc := strings.HasPrefix(ddocName, "dev_") switch namespace { case DesignDocumentNamespaceProduction: if isDevDoc { continue } case DesignDocumentNamespaceDevelopment: if !isDevDoc { continue } ddocName = strings.TrimPrefix(ddocName, "dev_") default: return nil, makeInvalidArgumentsError("design document namespace unknown") } var ddoc DesignDocument err := ddoc.fromData(ddocData.Doc.JSON, ddocName) if err != nil { return nil, err } ddocs = append(ddocs, ddoc) } return ddocs, nil } // UpsertDesignDocumentOptions is the set of options available to the ViewIndexManager UpsertDesignDocument operation. type UpsertDesignDocumentOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UpsertDesignDocument will insert a design document to the given bucket, or update // an existing design document with the same name. func (vm *ViewIndexManager) UpsertDesignDocument(ddoc DesignDocument, namespace DesignDocumentNamespace, opts *UpsertDesignDocumentOptions) error { if opts == nil { opts = &UpsertDesignDocumentOptions{} } start := time.Now() defer vm.meter.ValueRecord(meterValueServiceManagement, "manager_views_upsert_design_document", start) return vm.upsertDesignDocument(ddoc, namespace, time.Now(), opts) } func (vm *ViewIndexManager) upsertDesignDocument( ddoc DesignDocument, namespace DesignDocumentNamespace, startTime time.Time, opts *UpsertDesignDocumentOptions, ) error { ddocData, ddocName, err := ddoc.toData() if err != nil { return err } ddocName = vm.ddocName(ddocName, namespace) span := createSpan(vm.tracer, opts.ParentSpan, "manager_views_upsert_design_document", "management") span.SetAttribute("db.operation", "PUT "+fmt.Sprintf("/_design/%s", ddocName)) span.SetAttribute("db.name", vm.bucketName) defer span.End() espan := createSpan(vm.tracer, span, "request_encoding", "") data, err := json.Marshal(&ddocData) espan.End() if err != nil { return err } req := mgmtRequest{ Service: ServiceTypeViews, Path: fmt.Sprintf("/_design/%s", ddocName), Method: "PUT", Body: data, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, parentSpanCtx: span.Context(), UniqueID: uuid.New().String(), } resp, err := vm.doMgmtRequest(opts.Context, req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 201 { vwErr := vm.tryParseErrorMessage(req, resp) if vwErr != nil { return vwErr } return makeMgmtBadStatusError("failed to upsert design document", &req, resp) } return nil } // DropDesignDocumentOptions is the set of options available to the ViewIndexManager Upsert operation. type DropDesignDocumentOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropDesignDocument will remove a design document from the given bucket. func (vm *ViewIndexManager) DropDesignDocument(name string, namespace DesignDocumentNamespace, opts *DropDesignDocumentOptions) error { if opts == nil { opts = &DropDesignDocumentOptions{} } start := time.Now() defer vm.meter.ValueRecord(meterValueServiceManagement, "manager_views_drop_design_document", start) span := createSpan(vm.tracer, opts.ParentSpan, "manager_views_drop_design_document", "management") span.SetAttribute("db.operation", "DELETE "+fmt.Sprintf("/_design/%s", name)) span.SetAttribute("db.name", vm.bucketName) defer span.End() return vm.dropDesignDocument(span.Context(), name, namespace, time.Now(), opts) } func (vm *ViewIndexManager) dropDesignDocument(tracectx RequestSpanContext, name string, namespace DesignDocumentNamespace, startTime time.Time, opts *DropDesignDocumentOptions) error { name = vm.ddocName(name, namespace) req := mgmtRequest{ Service: ServiceTypeViews, Path: fmt.Sprintf("/_design/%s", name), Method: "DELETE", Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, parentSpanCtx: tracectx, UniqueID: uuid.New().String(), } resp, err := vm.doMgmtRequest(opts.Context, req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { vwErr := vm.tryParseErrorMessage(req, resp) if vwErr != nil { return vwErr } return makeMgmtBadStatusError("failed to drop design document", &req, resp) } return nil } // PublishDesignDocumentOptions is the set of options available to the ViewIndexManager PublishDesignDocument operation. type PublishDesignDocumentOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // PublishDesignDocument publishes a design document to the given bucket. func (vm *ViewIndexManager) PublishDesignDocument(name string, opts *PublishDesignDocumentOptions) error { startTime := time.Now() if opts == nil { opts = &PublishDesignDocumentOptions{} } start := time.Now() defer vm.meter.ValueRecord(meterValueServiceManagement, "manager_views_publish_design_document", start) span := createSpan(vm.tracer, opts.ParentSpan, "manager_views_publish_design_document", "management") span.SetAttribute("db.name", vm.bucketName) defer span.End() devdoc, err := vm.getDesignDocument( name, DesignDocumentNamespaceDevelopment, startTime, &GetDesignDocumentOptions{ RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } err = vm.upsertDesignDocument( *devdoc, DesignDocumentNamespaceProduction, startTime, &UpsertDesignDocumentOptions{ RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } gocb-2.6.3/bucket_viewindexes_test.go000066400000000000000000000270601441755043100177220ustar00rootroot00000000000000package gocb import ( "bytes" "errors" "fmt" "io/ioutil" "time" "github.com/stretchr/testify/mock" ) func (suite *IntegrationTestSuite) TestViewIndexManagerCrud() { suite.skipIfUnsupported(ViewFeature) suite.skipIfUnsupported(ViewIndexUpsertBugFeature) mgr := globalBucket.ViewIndexes() err := mgr.UpsertDesignDocument(DesignDocument{ Name: "test", Views: map[string]View{ "testView": { Map: `function(doc, meta) { emit(doc, null); }`, Reduce: "_count", }, }, }, DesignDocumentNamespaceDevelopment, &UpsertDesignDocumentOptions{}) suite.Require().Nil(err, err) var designdoc *DesignDocument numGetsStart := 0 success := suite.tryUntil(time.Now().Add(5*time.Second), 500*time.Millisecond, func() bool { numGetsStart++ designdoc, err = mgr.GetDesignDocument("test", DesignDocumentNamespaceDevelopment, &GetDesignDocumentOptions{}) if err != nil { return false } return true }) if !success { suite.T().Fatalf("Wait time for get design document expired.") } suite.Assert().Equal("test", designdoc.Name) suite.Require().Equal(1, len(designdoc.Views)) suite.Require().Contains(designdoc.Views, "testView") view := designdoc.Views["testView"] suite.Assert().NotEmpty(view.Map) suite.Assert().NotEmpty(view.Reduce) designdocs, err := mgr.GetAllDesignDocuments(DesignDocumentNamespaceDevelopment, &GetAllDesignDocumentsOptions{}) suite.Require().Nil(err, err) suite.Require().GreaterOrEqual(len(designdocs), 1) err = mgr.PublishDesignDocument("test", &PublishDesignDocumentOptions{}) suite.Require().Nil(err, err) // It can take time for the published doc to come online numGetsPublished := 0 success = suite.tryUntil(time.Now().Add(5*time.Second), 500*time.Millisecond, func() bool { numGetsPublished++ designdoc, err = mgr.GetDesignDocument("test", DesignDocumentNamespaceProduction, &GetDesignDocumentOptions{}) if err != nil { return false } return true }) if !success { suite.T().Fatalf("Wait time for get design document expired.") } suite.Assert().Equal("test", designdoc.Name) suite.Require().Equal(1, len(designdoc.Views)) err = mgr.DropDesignDocument("test", DesignDocumentNamespaceProduction, &DropDesignDocumentOptions{}) suite.Require().Nil(err, err) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(4+numGetsPublished+numGetsStart, len(nilParents)) suite.AssertHTTPOpSpan(nilParents[0], "manager_views_upsert_design_document", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, operationID: "PUT /_design/dev_test", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "management", }) suite.AssertHTTPOpSpan(nilParents[1], "manager_views_get_design_document", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, operationID: "GET /_design/dev_test", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "management", }) suite.AssertHTTPOpSpan(nilParents[numGetsStart+1], "manager_views_get_all_design_documents", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, operationID: "GET " + fmt.Sprintf("/pools/default/buckets/%s/ddocs", globalConfig.Bucket), numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "management", }) publishParents := nilParents[numGetsStart+2] suite.AssertHTTPSpan(publishParents, "manager_views_publish_design_document", globalConfig.Bucket, "", "", "management", "", "") suite.Require().Len(publishParents.Spans, 2) suite.Require().Contains(publishParents.Spans, "manager_views_get_design_document") suite.Require().Contains(publishParents.Spans, "manager_views_upsert_design_document") suite.AssertHTTPOpSpan(publishParents.Spans["manager_views_get_design_document"][0], "manager_views_get_design_document", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, operationID: "GET /_design/dev_test", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "management", }) suite.AssertHTTPOpSpan(publishParents.Spans["manager_views_upsert_design_document"][0], "manager_views_upsert_design_document", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, operationID: "PUT /_design/test", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "management", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_views_upsert_design_document"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_views_get_design_document"), 2, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_views_get_all_design_documents"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_views_drop_design_document"), 1, false) } func (suite *UnitTestSuite) TestViewIndexManagerGetDoesntExist() { ddocName := "ddoc" retErr := `{"error": "not_found", "reason": "missing}` resp := &mgmtResponse{ Endpoint: "http://localhost:8092/default", StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/_design/dev_"+ddocName, req.Path) suite.Assert().Equal(ServiceTypeViews, req.Service) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("GET", req.Method) suite.Assert().Nil(req.RetryStrategy) }). Return(resp, nil) viewMgr := ViewIndexManager{ mgmtProvider: mockProvider, bucketName: "mock", tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } _, err := viewMgr.GetDesignDocument(ddocName, DesignDocumentNamespaceDevelopment, &GetDesignDocumentOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrDesignDocumentNotFound) { suite.T().Fatalf("Expected design document not found: %s", err) } } func (suite *UnitTestSuite) TestViewIndexManagerPublishDoesntExist() { ddocName := "ddoc" retErr := `{"error": "not_found", "reason": "missing}` resp := &mgmtResponse{ Endpoint: "http://localhost:8092/default", StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/_design/dev_"+ddocName, req.Path) suite.Assert().Equal(ServiceTypeViews, req.Service) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("GET", req.Method) suite.Assert().Nil(req.RetryStrategy) }). Return(resp, nil) viewMgr := ViewIndexManager{ mgmtProvider: mockProvider, bucketName: "mock", tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } err := viewMgr.PublishDesignDocument(ddocName, &PublishDesignDocumentOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrDesignDocumentNotFound) { suite.T().Fatalf("Expected design document not found: %s", err) } } func (suite *UnitTestSuite) TestViewIndexManagerDropDoesntExist() { ddocName := "ddoc" retErr := `{"error": "not_found", "reason": "missing}` resp := &mgmtResponse{ Endpoint: "http://localhost:8092/default", StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/_design/"+ddocName, req.Path) suite.Assert().Equal(ServiceTypeViews, req.Service) suite.Assert().False(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("DELETE", req.Method) suite.Assert().Nil(req.RetryStrategy) }). Return(resp, nil) viewMgr := ViewIndexManager{ mgmtProvider: mockProvider, bucketName: "mock", tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } err := viewMgr.DropDesignDocument(ddocName, DesignDocumentNamespaceProduction, &DropDesignDocumentOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrDesignDocumentNotFound) { suite.T().Fatalf("Expected design document not found: %s", err) } } func (suite *UnitTestSuite) TestViewIndexManagerGetAllDesignDocumentsFiltersCorrectlyProduction() { payload, err := loadRawTestDataset("views_response_70") suite.Require().Nil(err) resp := &mgmtResponse{ Endpoint: "http://localhost:8092/default", StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(payload)), } mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/pools/default/buckets/mock/ddocs", req.Path) suite.Assert().Equal(ServiceTypeManagement, req.Service) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("GET", req.Method) suite.Assert().Nil(req.RetryStrategy) }). Return(resp, nil) viewMgr := ViewIndexManager{ mgmtProvider: mockProvider, bucketName: "mock", tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } ddocs, err := viewMgr.GetAllDesignDocuments(DesignDocumentNamespaceProduction, &GetAllDesignDocumentsOptions{ Timeout: 1 * time.Second, }) suite.Require().Nil(err) suite.Require().Len(ddocs, 1) suite.Assert().Equal("aaa", ddocs[0].Name) } func (suite *UnitTestSuite) TestViewIndexManagerGetAllDesignDocumentsFiltersCorrectlyDevelopment() { payload, err := loadRawTestDataset("views_response_70") suite.Require().Nil(err) resp := &mgmtResponse{ Endpoint: "http://localhost:8092/default", StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(payload)), } mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/pools/default/buckets/mock/ddocs", req.Path) suite.Assert().Equal(ServiceTypeManagement, req.Service) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("GET", req.Method) suite.Assert().Nil(req.RetryStrategy) }). Return(resp, nil) viewMgr := ViewIndexManager{ mgmtProvider: mockProvider, bucketName: "mock", tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } ddocs, err := viewMgr.GetAllDesignDocuments(DesignDocumentNamespaceDevelopment, &GetAllDesignDocumentsOptions{ Timeout: 1 * time.Second, }) suite.Require().Nil(err) suite.Require().Len(ddocs, 3) suite.Assert().Equal("aaa", ddocs[0].Name) suite.Assert().Equal("test", ddocs[1].Name) suite.Assert().Equal("test12", ddocs[2].Name) } gocb-2.6.3/bucket_viewquery.go000066400000000000000000000161421441755043100163700ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "net/url" "strings" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) type jsonViewResponse struct { TotalRows uint64 `json:"total_rows,omitempty"` DebugInfo interface{} `json:"debug_info,omitempty"` } type jsonViewRow struct { ID string `json:"id"` Key json.RawMessage `json:"key"` Value json.RawMessage `json:"value"` } // ViewMetaData provides access to the meta-data properties of a view query result. type ViewMetaData struct { TotalRows uint64 Debug interface{} } func (meta *ViewMetaData) fromData(data jsonViewResponse) error { meta.TotalRows = data.TotalRows meta.Debug = data.DebugInfo return nil } // ViewRow represents a single row returned from a view query. type ViewRow struct { ID string keyBytes []byte valueBytes []byte } // Key returns the key associated with this view row. func (vr *ViewRow) Key(valuePtr interface{}) error { return json.Unmarshal(vr.keyBytes, valuePtr) } // Value returns the value associated with this view row. func (vr *ViewRow) Value(valuePtr interface{}) error { return json.Unmarshal(vr.valueBytes, valuePtr) } type viewRowReader interface { NextRow() []byte Err() error MetaData() ([]byte, error) Close() error } // ViewResultRaw provides raw access to views data. // VOLATILE: This API is subject to change at any time. type ViewResultRaw struct { reader viewRowReader } // NextBytes returns the next row as bytes. func (vrr *ViewResultRaw) NextBytes() []byte { return vrr.reader.NextRow() } // Err returns any errors that have occurred on the stream func (vrr *ViewResultRaw) Err() error { err := vrr.reader.Err() if err != nil { return maybeEnhanceViewError(err) } return nil } // Close marks the results as closed, returning any errors that occurred during reading the results. func (vrr *ViewResultRaw) Close() error { err := vrr.reader.Close() if err != nil { return maybeEnhanceViewError(err) } return nil } // MetaData returns any meta-data that was available from this query as bytes. func (vrr *ViewResultRaw) MetaData() ([]byte, error) { return vrr.reader.MetaData() } // ViewResult implements an iterator interface which can be used to iterate over the rows of the query results. type ViewResult struct { reader viewRowReader currentRow ViewRow jsonErr error } func newViewResult(reader viewRowReader) *ViewResult { return &ViewResult{ reader: reader, } } // Raw returns a ViewResultRaw which can be used to access the raw byte data from view queries. // Calling this function invalidates the underlying ViewResult which will no longer be able to be used. // VOLATILE: This API is subject to change at any time. func (r *ViewResult) Raw() *ViewResultRaw { vr := &ViewResultRaw{ reader: r.reader, } r.reader = nil return vr } // Next assigns the next result from the results into the value pointer, returning whether the read was successful. func (r *ViewResult) Next() bool { if r.reader == nil { return false } rowBytes := r.reader.NextRow() if rowBytes == nil { return false } r.currentRow = ViewRow{} var rowData jsonViewRow if err := json.Unmarshal(rowBytes, &rowData); err != nil { // This should never happen but if it does then lets store it in a best efforts basis and maybe the next // row will be ok. We can then return this from .Err(). r.jsonErr = err return true } r.currentRow.ID = rowData.ID r.currentRow.keyBytes = rowData.Key r.currentRow.valueBytes = rowData.Value return true } // Row returns the contents of the current row. func (r *ViewResult) Row() ViewRow { if r.reader == nil { return ViewRow{} } return r.currentRow } // Err returns any errors that have occurred on the stream func (r *ViewResult) Err() error { if r.reader == nil { return errors.New("result object is no longer valid") } err := r.reader.Err() if err != nil { return maybeEnhanceViewError(err) } // This is an error from json unmarshal so no point in trying to enhance it. return r.jsonErr } // Close marks the results as closed, returning any errors that occurred during reading the results. func (r *ViewResult) Close() error { if r.reader == nil { return r.Err() } err := r.reader.Close() if err != nil { return maybeEnhanceViewError(err) } return nil } // MetaData returns any meta-data that was available from this query. Note that // the meta-data will only be available once the object has been closed (either // implicitly or explicitly). func (r *ViewResult) MetaData() (*ViewMetaData, error) { if r.reader == nil { return nil, r.Err() } metaDataBytes, err := r.reader.MetaData() if err != nil { return nil, err } var jsonResp jsonViewResponse err = json.Unmarshal(metaDataBytes, &jsonResp) if err != nil { return nil, err } var metaData ViewMetaData err = metaData.fromData(jsonResp) if err != nil { return nil, err } return &metaData, nil } // ViewQuery performs a view query and returns a list of rows or an error. func (b *Bucket) ViewQuery(designDoc string, viewName string, opts *ViewOptions) (*ViewResult, error) { if opts == nil { opts = &ViewOptions{} } start := time.Now() defer b.meter.ValueRecord(meterValueServiceViews, "views", start) designDoc = b.maybePrefixDevDocument(opts.Namespace, designDoc) span := createSpan(b.tracer, opts.ParentSpan, "views", "views") span.SetAttribute("db.name", b.Name()) span.SetAttribute("db.operation", designDoc+"/"+viewName) defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = b.timeoutsConfig.ViewTimeout } deadline := time.Now().Add(timeout) retryWrapper := b.retryStrategyWrapper if opts.RetryStrategy != nil { retryWrapper = newRetryStrategyWrapper(opts.RetryStrategy) } urlValues, err := opts.toURLValues() if err != nil { return nil, wrapError(err, "could not parse query options") } return b.execViewQuery(opts.Context, span.Context(), "_view", designDoc, viewName, *urlValues, deadline, retryWrapper, opts.Internal.User) } func (b *Bucket) execViewQuery( ctx context.Context, span RequestSpanContext, viewType, ddoc, viewName string, options url.Values, deadline time.Time, wrapper *retryStrategyWrapper, user string, ) (*ViewResult, error) { provider, err := b.connectionManager.getViewProvider(b.Name()) if err != nil { return nil, ViewError{ InnerError: wrapError(err, "failed to get query provider"), DesignDocumentName: ddoc, ViewName: viewName, } } res, err := provider.ViewQuery(ctx, gocbcore.ViewQueryOptions{ DesignDocumentName: ddoc, ViewType: viewType, ViewName: viewName, Options: options, RetryStrategy: wrapper, Deadline: deadline, TraceContext: span, User: user, }) if err != nil { return nil, maybeEnhanceViewError(err) } return newViewResult(res), nil } func (b *Bucket) maybePrefixDevDocument(namespace DesignDocumentNamespace, ddoc string) string { designDoc := ddoc if namespace == DesignDocumentNamespaceProduction { designDoc = strings.TrimPrefix(ddoc, "dev_") } else { if !strings.HasPrefix(ddoc, "dev_") { designDoc = "dev_" + ddoc } } return designDoc } gocb-2.6.3/bucket_viewquery_test.go000066400000000000000000000203151441755043100174240ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "time" "github.com/couchbase/gocbcore/v10" "github.com/stretchr/testify/mock" ) func (suite *IntegrationTestSuite) TestViewQuery() { suite.skipIfUnsupported(ViewFeature) suite.skipIfUnsupported(ViewIndexUpsertBugFeature) n := suite.setupViews() suite.runViewsTest(n) } func (suite *IntegrationTestSuite) runViewsTest(n int) { deadline := time.Now().Add(60 * time.Second) var result *ViewResult for { globalTracer.Reset() globalMeter.Reset() var err error result, err = globalBucket.ViewQuery("ddoc_test", "test", &ViewOptions{ Timeout: 1 * time.Second, Namespace: DesignDocumentNamespaceDevelopment, Debug: true, }) if err != nil { sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for indexing") return } continue } samples := make(map[string]testBreweryDocument) for result.Next() { row := result.Row() suite.Assert().NotEmpty(row.ID) var val testBreweryDocument err := row.Value(&val) suite.Require().Nil(err, err) suite.Assert().NotNil(val) var key string err = row.Key(&key) suite.Require().Nil(err, err) suite.Assert().NotEmpty(key) samples[key] = val } if len(samples) == n { break } err = result.Err() suite.Require().Nil(err, "Result had error %v", err) sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for indexing") return } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(1, len(nilParents)) suite.AssertHTTPOpSpan(nilParents[0], "views", HTTPOpSpanExpectations{ bucket: globalConfig.Bucket, operationID: "dev_ddoc_test/test", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "", service: "views", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "views", "views"), 1, false) } metadata, err := result.MetaData() suite.Require().Nil(err, "Metadata had error: %v", err) suite.Assert().NotEmpty(metadata.TotalRows) suite.Assert().NotEmpty(metadata.Debug) } func (suite *IntegrationTestSuite) setupViews() int { n, err := suite.createBreweryDataset("beer_sample_brewery_five", "views", "", "") suite.Require().Nil(err, err) mgr := globalBucket.ViewIndexes() err = mgr.UpsertDesignDocument(DesignDocument{ Name: "ddoc_test", Views: map[string]View{ "test": { Map: ` function (doc, meta) { if (doc.service === "views") { emit(meta.id, doc); } }`, Reduce: "_count", }, }, }, DesignDocumentNamespaceDevelopment, &UpsertDesignDocumentOptions{ Timeout: 1 * time.Second, }) suite.Require().Nil(err, err) return n } func (suite *IntegrationTestSuite) TestViewQueryContext() { suite.skipIfUnsupported(SearchFeature) suite.skipIfServerVersionEquals(srvVer750) ctx, cancel := context.WithCancel(context.Background()) cancel() res, err := globalBucket.ViewQuery("test", "test", &ViewOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) defer cancel() res, err = globalBucket.ViewQuery("test", "test", &ViewOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) } // We have to manually mock this because testify won't let return something which can iterate. type mockViewRowReader struct { Dataset []jsonViewRow Meta []byte MetaErr error CloseErr error RowsErr error Suite *UnitTestSuite idx int } func (arr *mockViewRowReader) NextRow() []byte { if arr.idx == len(arr.Dataset) { return nil } idx := arr.idx arr.idx++ return arr.Suite.mustConvertToBytes(arr.Dataset[idx]) } func (arr *mockViewRowReader) MetaData() ([]byte, error) { return arr.Meta, arr.MetaErr } func (arr *mockViewRowReader) Close() error { return arr.CloseErr } func (arr *mockViewRowReader) Err() error { return arr.RowsErr } type testViewDataset struct { Rows []jsonViewRow jsonViewResponse } func (suite *UnitTestSuite) viewsBucket(reader viewRowReader, runFn func(args mock.Arguments)) *Bucket { provider := new(mockViewProvider) provider. On("ViewQuery", nil, mock.AnythingOfType("gocbcore.ViewQueryOptions")). Run(runFn). Return(reader, nil) cli := new(mockConnectionManager) cli.On("getViewProvider", "mockBucket").Return(provider, nil) cluster := suite.newCluster(cli) b := newBucket(cluster, "mockBucket") return b } func (suite *UnitTestSuite) TestViewQuery() { var dataset testViewDataset err := loadJSONTestDataset("beer_sample_views_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockViewRowReader{ Dataset: dataset.Rows, Meta: suite.mustConvertToBytes(dataset.jsonViewResponse), Suite: suite, } var bucket *Bucket bucket = suite.viewsBucket(reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.ViewQueryOptions) suite.Assert().Equal(bucket.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } suite.Assert().Equal("dev_ddoc", opts.DesignDocumentName) suite.Assert().Equal("view", opts.ViewName) suite.Assert().Equal("_view", opts.ViewType) suite.Assert().Equal("56", opts.Options.Get("skip")) suite.Assert().Equal("10", opts.Options.Get("limit")) suite.Assert().Equal("true", opts.Options.Get("debug")) }) result, err := bucket.ViewQuery("ddoc", "view", &ViewOptions{ Namespace: DesignDocumentNamespaceDevelopment, Skip: 56, Limit: 10, Debug: true, }) suite.Require().Nil(err, err) suite.Require().NotNil(result) expectedBreweries := make(map[string]testBreweryDocument) for _, brewery := range dataset.Rows { var key string suite.Require().Nil(json.Unmarshal(brewery.Key, &key)) var val testBreweryDocument suite.Require().Nil(json.Unmarshal(brewery.Value, &val)) expectedBreweries[key] = val } actualBreweries := make(map[string]testBreweryDocument) for result.Next() { row := result.Row() var key string suite.Require().Nil(row.Key(&key)) var val testBreweryDocument suite.Require().Nil(row.Value(&val)) actualBreweries[key] = val } suite.Assert().Equal(expectedBreweries, actualBreweries) meta, err := result.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(dataset.TotalRows, meta.TotalRows) suite.Assert().Equal(dataset.DebugInfo, meta.Debug) } func (suite *UnitTestSuite) TestViewQueryRaw() { var dataset testViewDataset err := loadJSONTestDataset("beer_sample_views_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockViewRowReader{ Dataset: dataset.Rows, Meta: suite.mustConvertToBytes(dataset.jsonViewResponse), Suite: suite, } var bucket *Bucket bucket = suite.viewsBucket(reader, func(args mock.Arguments) {}) result, err := bucket.ViewQuery("ddoc", "view", &ViewOptions{ Namespace: DesignDocumentNamespaceDevelopment, Skip: 56, Limit: 10, Debug: true, }) suite.Require().Nil(err, err) suite.Require().NotNil(result) raw := result.Raw() suite.Assert().False(result.Next()) suite.Assert().Error(result.Err()) suite.Assert().Error(result.Close()) suite.Assert().Zero(result.Row()) _, err = result.MetaData() suite.Assert().Error(err) var i int for b := raw.NextBytes(); b != nil; b = raw.NextBytes() { suite.Assert().Equal(suite.mustConvertToBytes(dataset.Rows[i]), b) i++ } err = raw.Err() suite.Require().Nil(err, err) metadata, err := raw.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(reader.Meta, metadata) } gocb-2.6.3/circuitbreaker.go000066400000000000000000000011361441755043100157660ustar00rootroot00000000000000package gocb import "time" // CircuitBreakerCallback is the callback used by the circuit breaker to determine if an error should count toward // the circuit breaker failure count. type CircuitBreakerCallback func(error) bool // CircuitBreakerConfig are the settings for configuring circuit breakers. type CircuitBreakerConfig struct { Disabled bool VolumeThreshold int64 ErrorThresholdPercentage float64 SleepWindow time.Duration RollingWindow time.Duration CompletionCallback CircuitBreakerCallback CanaryTimeout time.Duration } gocb-2.6.3/client.go000066400000000000000000000176751441755043100142650ustar00rootroot00000000000000package gocb import ( "crypto/x509" "errors" "sync" "github.com/couchbase/gocbcore/v10" ) type connectionManager interface { connect() error openBucket(bucketName string) error buildConfig(cluster *Cluster) error getKvProvider(bucketName string) (kvProvider, error) getKvCapabilitiesProvider(bucketName string) (kvCapabilityVerifier, error) getViewProvider(bucketName string) (viewProvider, error) getQueryProvider() (queryProvider, error) getAnalyticsProvider() (analyticsProvider, error) getSearchProvider() (searchProvider, error) getHTTPProvider(bucketName string) (httpProvider, error) getDiagnosticsProvider(bucketName string) (diagnosticsProvider, error) getWaitUntilReadyProvider(bucketName string) (waitUntilReadyProvider, error) connection(bucketName string) (*gocbcore.Agent, error) close() error } type stdConnectionMgr struct { lock sync.Mutex agentgroup *gocbcore.AgentGroup config *gocbcore.AgentGroupConfig } func newConnectionMgr() *stdConnectionMgr { client := &stdConnectionMgr{} return client } func (c *stdConnectionMgr) buildConfig(cluster *Cluster) error { c.lock.Lock() defer c.lock.Unlock() breakerCfg := cluster.circuitBreakerConfig var completionCallback func(err error) bool if breakerCfg.CompletionCallback != nil { completionCallback = func(err error) bool { wrappedErr := maybeEnhanceKVErr(err, "", "", "", "") return breakerCfg.CompletionCallback(wrappedErr) } } var authMechanisms []gocbcore.AuthMechanism for _, mech := range cluster.securityConfig.AllowedSaslMechanisms { authMechanisms = append(authMechanisms, gocbcore.AuthMechanism(mech)) } config := &gocbcore.AgentGroupConfig{ AgentConfig: gocbcore.AgentConfig{ UserAgent: Identifier(), SecurityConfig: gocbcore.SecurityConfig{ AuthMechanisms: authMechanisms, }, IoConfig: gocbcore.IoConfig{ UseCollections: true, UseDurations: cluster.useServerDurations, UseMutationTokens: cluster.useMutationTokens, UseOutOfOrderResponses: true, }, KVConfig: gocbcore.KVConfig{ ConnectTimeout: cluster.timeoutsConfig.ConnectTimeout, ConnectionBufferSize: cluster.internalConfig.ConnectionBufferSize, }, DefaultRetryStrategy: cluster.retryStrategyWrapper, CircuitBreakerConfig: gocbcore.CircuitBreakerConfig{ Enabled: !breakerCfg.Disabled, VolumeThreshold: breakerCfg.VolumeThreshold, ErrorThresholdPercentage: breakerCfg.ErrorThresholdPercentage, SleepWindow: breakerCfg.SleepWindow, RollingWindow: breakerCfg.RollingWindow, CanaryTimeout: breakerCfg.CanaryTimeout, CompletionCallback: completionCallback, }, OrphanReporterConfig: gocbcore.OrphanReporterConfig{ Enabled: cluster.orphanLoggerEnabled, ReportInterval: cluster.orphanLoggerInterval, SampleSize: int(cluster.orphanLoggerSampleSize), }, TracerConfig: gocbcore.TracerConfig{ NoRootTraceSpans: true, Tracer: &coreRequestTracerWrapper{tracer: cluster.tracer}, }, MeterConfig: gocbcore.MeterConfig{ // At the moment we only support our own operations metric so there's no point in setting a meter for gocbcore. Meter: nil, }, CompressionConfig: gocbcore.CompressionConfig{ Enabled: !cluster.compressionConfig.Disabled, MinSize: int(cluster.compressionConfig.MinSize), MinRatio: cluster.compressionConfig.MinRatio, }, }, } err := config.FromConnStr(cluster.connSpec().String()) if err != nil { return err } config.SecurityConfig.Auth = &coreAuthWrapper{ auth: cluster.authenticator(), } if config.SecurityConfig.UseTLS { config.SecurityConfig.TLSRootCAProvider = cluster.internalConfig.TLSRootCAProvider if config.SecurityConfig.TLSRootCAProvider == nil && (cluster.securityConfig.TLSRootCAs != nil || cluster.securityConfig.TLSSkipVerify) { config.SecurityConfig.TLSRootCAProvider = func() *x509.CertPool { if cluster.securityConfig.TLSSkipVerify { return nil } return cluster.securityConfig.TLSRootCAs } } } c.config = config return nil } func (c *stdConnectionMgr) connect() error { c.lock.Lock() defer c.lock.Unlock() var err error c.agentgroup, err = gocbcore.CreateAgentGroup(c.config) if err != nil { return maybeEnhanceKVErr(err, "", "", "", "") } return nil } func (c *stdConnectionMgr) openBucket(bucketName string) error { if c.agentgroup == nil { return errors.New("cluster not yet connected") } return c.agentgroup.OpenBucket(bucketName) } func (c *stdConnectionMgr) getKvProvider(bucketName string) (kvProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("bucket not yet connected") } return agent, nil } func (c *stdConnectionMgr) getKvCapabilitiesProvider(bucketName string) (kvCapabilityVerifier, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("bucket not yet connected") } return agent.Internal(), nil } func (c *stdConnectionMgr) getViewProvider(bucketName string) (viewProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("bucket not yet connected") } return &viewProviderWrapper{provider: agent}, nil } func (c *stdConnectionMgr) getQueryProvider() (queryProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } return &queryProviderWrapper{provider: c.agentgroup}, nil } func (c *stdConnectionMgr) getAnalyticsProvider() (analyticsProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } return &analyticsProviderWrapper{provider: c.agentgroup}, nil } func (c *stdConnectionMgr) getSearchProvider() (searchProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } return &searchProviderWrapper{provider: c.agentgroup}, nil } func (c *stdConnectionMgr) getHTTPProvider(bucketName string) (httpProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } if bucketName == "" { return &httpProviderWrapper{provider: c.agentgroup}, nil } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("bucket not yet connected") } return &httpProviderWrapper{provider: agent}, nil } func (c *stdConnectionMgr) getDiagnosticsProvider(bucketName string) (diagnosticsProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } if bucketName == "" { return &diagnosticsProviderWrapper{provider: c.agentgroup}, nil } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("bucket not yet connected") } return &diagnosticsProviderWrapper{provider: agent}, nil } func (c *stdConnectionMgr) getWaitUntilReadyProvider(bucketName string) (waitUntilReadyProvider, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } if bucketName == "" { return &waitUntilReadyProviderWrapper{provider: c.agentgroup}, nil } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("provider not yet connected") } return &waitUntilReadyProviderWrapper{provider: agent}, nil } func (c *stdConnectionMgr) connection(bucketName string) (*gocbcore.Agent, error) { if c.agentgroup == nil { return nil, errors.New("cluster not yet connected") } agent := c.agentgroup.GetAgent(bucketName) if agent == nil { return nil, errors.New("bucket not yet connected") } return agent, nil } func (c *stdConnectionMgr) close() error { c.lock.Lock() if c.agentgroup == nil { c.lock.Unlock() return errors.New("cluster not yet connected") } defer c.lock.Unlock() return c.agentgroup.Close() } gocb-2.6.3/cluster.go000066400000000000000000000415741441755043100144630ustar00rootroot00000000000000package gocb import ( "context" "crypto/x509" "errors" "fmt" "strconv" "time" gocbcore "github.com/couchbase/gocbcore/v10" gocbconnstr "github.com/couchbase/gocbcore/v10/connstr" ) // Cluster represents a connection to a specific Couchbase cluster. type Cluster struct { cSpec gocbconnstr.ConnSpec auth Authenticator connectionManager connectionManager useServerDurations bool useMutationTokens bool timeoutsConfig TimeoutsConfig transcoder Transcoder retryStrategyWrapper *retryStrategyWrapper orphanLoggerEnabled bool orphanLoggerInterval time.Duration orphanLoggerSampleSize uint32 tracer RequestTracer meter *meterWrapper circuitBreakerConfig CircuitBreakerConfig securityConfig SecurityConfig internalConfig InternalConfig transactionsConfig TransactionsConfig compressionConfig CompressionConfig transactions *Transactions } // IoConfig specifies IO related configuration options. type IoConfig struct { DisableMutationTokens bool DisableServerDurations bool } // TimeoutsConfig specifies options for various operation timeouts. type TimeoutsConfig struct { ConnectTimeout time.Duration KVTimeout time.Duration KVDurableTimeout time.Duration // Volatile: This option is subject to change at any time. KVScanTimeout time.Duration ViewTimeout time.Duration QueryTimeout time.Duration AnalyticsTimeout time.Duration SearchTimeout time.Duration ManagementTimeout time.Duration } // OrphanReporterConfig specifies options for controlling the orphan // reporter which records when the SDK receives responses for requests // that are no longer in the system (usually due to being timed out). type OrphanReporterConfig struct { Disabled bool ReportInterval time.Duration SampleSize uint32 } // SecurityConfig specifies options for controlling security related // items such as TLS root certificates and verification skipping. type SecurityConfig struct { TLSRootCAs *x509.CertPool TLSSkipVerify bool // AllowedSaslMechanisms is the list of mechanisms that the SDK can use to attempt authentication. // Note that if you add PLAIN to the list, this will cause credential leakage on the network // since PLAIN sends the credentials in cleartext. It is disabled by default to prevent downgrade attacks. We // recommend using a TLS connection if using PLAIN. AllowedSaslMechanisms []SaslMechanism } // CompressionConfig specifies options for controlling compression applied to documents before sending to Couchbase // Server. type CompressionConfig struct { Disabled bool // MinSize specifies the minimum size of the document to consider compression. MinSize uint32 // MinRatio specifies the minimal compress ratio (compressed / original) for the document to be sent compressed. MinRatio float64 } // InternalConfig specifies options for controlling various internal // items. // Internal: This should never be used and is not supported. type InternalConfig struct { TLSRootCAProvider func() *x509.CertPool ConnectionBufferSize uint } // ClusterOptions is the set of options available for creating a Cluster. type ClusterOptions struct { // Authenticator specifies the authenticator to use with the cluster. Authenticator Authenticator // Username & Password specifies the cluster username and password to // authenticate with. This is equivalent to passing PasswordAuthenticator // as the Authenticator parameter with the same values. Username string Password string // Timeouts specifies various operation timeouts. TimeoutsConfig TimeoutsConfig // Transcoder is used for trancoding data used in KV operations. Transcoder Transcoder // RetryStrategy is used to automatically retry operations if they fail. RetryStrategy RetryStrategy // Tracer specifies the tracer to use for requests. Tracer RequestTracer Meter Meter // OrphanReporterConfig specifies options for the orphan reporter. OrphanReporterConfig OrphanReporterConfig // CircuitBreakerConfig specifies options for the circuit breakers. CircuitBreakerConfig CircuitBreakerConfig // IoConfig specifies IO related configuration options. IoConfig IoConfig // SecurityConfig specifies security related configuration options. SecurityConfig SecurityConfig // TransactionsConfig specifies transactions related configuration options. TransactionsConfig TransactionsConfig // CompressionConfig specifies compression related configuration options. CompressionConfig CompressionConfig // Internal: This should never be used and is not supported. InternalConfig InternalConfig } // ClusterCloseOptions is the set of options available when // disconnecting from a Cluster. type ClusterCloseOptions struct { } func clusterFromOptions(opts ClusterOptions) *Cluster { if opts.Authenticator == nil { opts.Authenticator = PasswordAuthenticator{ Username: opts.Username, Password: opts.Password, } } connectTimeout := 10000 * time.Millisecond kvTimeout := 2500 * time.Millisecond kvDurableTimeout := 10000 * time.Millisecond kvScanTimeout := 10000 * time.Millisecond viewTimeout := 75000 * time.Millisecond queryTimeout := 75000 * time.Millisecond analyticsTimeout := 75000 * time.Millisecond searchTimeout := 75000 * time.Millisecond managementTimeout := 75000 * time.Millisecond if opts.TimeoutsConfig.ConnectTimeout > 0 { connectTimeout = opts.TimeoutsConfig.ConnectTimeout } if opts.TimeoutsConfig.KVTimeout > 0 { kvTimeout = opts.TimeoutsConfig.KVTimeout } if opts.TimeoutsConfig.KVDurableTimeout > 0 { kvDurableTimeout = opts.TimeoutsConfig.KVDurableTimeout } if opts.TimeoutsConfig.KVScanTimeout > 0 { kvScanTimeout = opts.TimeoutsConfig.KVScanTimeout } if opts.TimeoutsConfig.ViewTimeout > 0 { viewTimeout = opts.TimeoutsConfig.ViewTimeout } if opts.TimeoutsConfig.QueryTimeout > 0 { queryTimeout = opts.TimeoutsConfig.QueryTimeout } if opts.TimeoutsConfig.AnalyticsTimeout > 0 { analyticsTimeout = opts.TimeoutsConfig.AnalyticsTimeout } if opts.TimeoutsConfig.SearchTimeout > 0 { searchTimeout = opts.TimeoutsConfig.SearchTimeout } if opts.TimeoutsConfig.ManagementTimeout > 0 { managementTimeout = opts.TimeoutsConfig.ManagementTimeout } if opts.Transcoder == nil { opts.Transcoder = NewJSONTranscoder() } if opts.RetryStrategy == nil { opts.RetryStrategy = NewBestEffortRetryStrategy(nil) } useMutationTokens := true useServerDurations := true if opts.IoConfig.DisableMutationTokens { useMutationTokens = false } if opts.IoConfig.DisableServerDurations { useServerDurations = false } var initialTracer RequestTracer if opts.Tracer != nil { initialTracer = opts.Tracer } else { initialTracer = NewThresholdLoggingTracer(nil) } tracerAddRef(initialTracer) meter := opts.Meter if meter == nil { agMeter := NewLoggingMeter(nil) meter = agMeter } return &Cluster{ auth: opts.Authenticator, timeoutsConfig: TimeoutsConfig{ ConnectTimeout: connectTimeout, QueryTimeout: queryTimeout, AnalyticsTimeout: analyticsTimeout, SearchTimeout: searchTimeout, ViewTimeout: viewTimeout, KVTimeout: kvTimeout, KVDurableTimeout: kvDurableTimeout, KVScanTimeout: kvScanTimeout, ManagementTimeout: managementTimeout, }, transcoder: opts.Transcoder, useMutationTokens: useMutationTokens, retryStrategyWrapper: newRetryStrategyWrapper(opts.RetryStrategy), orphanLoggerEnabled: !opts.OrphanReporterConfig.Disabled, orphanLoggerInterval: opts.OrphanReporterConfig.ReportInterval, orphanLoggerSampleSize: opts.OrphanReporterConfig.SampleSize, useServerDurations: useServerDurations, tracer: initialTracer, meter: newMeterWrapper(meter), circuitBreakerConfig: opts.CircuitBreakerConfig, securityConfig: opts.SecurityConfig, internalConfig: opts.InternalConfig, transactionsConfig: opts.TransactionsConfig, compressionConfig: opts.CompressionConfig, } } // Connect creates and returns a Cluster instance created using the // provided options and a connection string. func Connect(connStr string, opts ClusterOptions) (*Cluster, error) { connSpec, err := gocbconnstr.Parse(connStr) if err != nil { return nil, err } if connSpec.Scheme == "http" { return nil, errors.New("http scheme is not supported, use couchbase or couchbases instead") } cluster := clusterFromOptions(opts) cluster.cSpec = connSpec err = cluster.parseExtraConnStrOptions(connSpec) if err != nil { return nil, err } cli := newConnectionMgr() err = cli.buildConfig(cluster) if err != nil { return nil, err } err = cli.connect() if err != nil { return nil, err } cluster.connectionManager = cli cluster.transactions, err = cluster.initTransactions(cluster.transactionsConfig) if err != nil { return nil, err } return cluster, nil } func (c *Cluster) parseExtraConnStrOptions(spec gocbconnstr.ConnSpec) error { fetchOption := func(name string) (string, bool) { optValue := spec.Options[name] if len(optValue) == 0 { return "", false } return optValue[len(optValue)-1], true } if valStr, ok := fetchOption("kv_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("query_timeout option must be a number") } c.timeoutsConfig.KVTimeout = time.Duration(val) * time.Millisecond } if valStr, ok := fetchOption("kv_durable_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("query_timeout option must be a number") } c.timeoutsConfig.KVDurableTimeout = time.Duration(val) * time.Millisecond } // Volatile: This option is subject to change at any time. if valStr, ok := fetchOption("kv_scan_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("query_timeout option must be a number") } c.timeoutsConfig.KVScanTimeout = time.Duration(val) * time.Millisecond } if valStr, ok := fetchOption("query_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("query_timeout option must be a number") } c.timeoutsConfig.QueryTimeout = time.Duration(val) * time.Millisecond } if valStr, ok := fetchOption("analytics_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("analytics_timeout option must be a number") } c.timeoutsConfig.AnalyticsTimeout = time.Duration(val) * time.Millisecond } if valStr, ok := fetchOption("search_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("search_timeout option must be a number") } c.timeoutsConfig.SearchTimeout = time.Duration(val) * time.Millisecond } if valStr, ok := fetchOption("view_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("view_timeout option must be a number") } c.timeoutsConfig.ViewTimeout = time.Duration(val) * time.Millisecond } if valStr, ok := fetchOption("management_timeout"); ok { val, err := strconv.ParseInt(valStr, 10, 64) if err != nil { return fmt.Errorf("view_timeout option must be a number") } c.timeoutsConfig.ManagementTimeout = time.Duration(val) * time.Millisecond } return nil } // Bucket connects the cluster to server(s) and returns a new Bucket instance. func (c *Cluster) Bucket(bucketName string) *Bucket { b := newBucket(c, bucketName) err := c.connectionManager.openBucket(bucketName) if err != nil { b.setBootstrapError(err) } return b } func (c *Cluster) authenticator() Authenticator { return c.auth } func (c *Cluster) connSpec() gocbconnstr.ConnSpec { return c.cSpec } // WaitUntilReadyOptions is the set of options available to the WaitUntilReady operations. type WaitUntilReadyOptions struct { DesiredState ClusterState ServiceTypes []ServiceType // Using a deadlined Context with WaitUntilReady will cause the shorter of the provided timeout and context deadline // to cause cancellation. Context context.Context // VOLATILE: This API is subject to change at any time. RetryStrategy RetryStrategy } // WaitUntilReady will wait for the cluster object to be ready for use. // At present this will wait until memd connections have been established with the server and are ready // to be used before performing a ping against the specified services which also // exist in the cluster map. // If no services are specified then ServiceTypeManagement, ServiceTypeQuery, ServiceTypeSearch, ServiceTypeAnalytics // will be pinged. // Valid service types are: ServiceTypeManagement, ServiceTypeQuery, ServiceTypeSearch, ServiceTypeAnalytics. func (c *Cluster) WaitUntilReady(timeout time.Duration, opts *WaitUntilReadyOptions) error { if opts == nil { opts = &WaitUntilReadyOptions{} } cli := c.connectionManager if cli == nil { return errors.New("cluster is not connected") } provider, err := cli.getWaitUntilReadyProvider("") if err != nil { return err } desiredState := opts.DesiredState if desiredState == 0 { desiredState = ClusterStateOnline } gocbcoreServices := make([]gocbcore.ServiceType, len(opts.ServiceTypes)) for i, svc := range opts.ServiceTypes { gocbcoreServices[i] = gocbcore.ServiceType(svc) } wrapper := c.retryStrategyWrapper if opts.RetryStrategy != nil { wrapper = newRetryStrategyWrapper(opts.RetryStrategy) } err = provider.WaitUntilReady( opts.Context, time.Now().Add(timeout), gocbcore.WaitUntilReadyOptions{ DesiredState: gocbcore.ClusterState(desiredState), ServiceTypes: gocbcoreServices, RetryStrategy: wrapper, }, ) if err != nil { return maybeEnhanceCoreErr(err) } return nil } // Close shuts down all buckets in this cluster and invalidates any references this cluster has. func (c *Cluster) Close(opts *ClusterCloseOptions) error { var overallErr error // This needs to be closed first. if c.transactions != nil { err := c.transactions.close() if err != nil { logWarnf("Failed to close transactions in cluster close: %s", err) } c.transactions = nil } if c.connectionManager != nil { err := c.connectionManager.close() if err != nil { logWarnf("Failed to close cluster connectionManager in cluster close: %s", err) overallErr = err } } if c.tracer != nil { tracerDecRef(c.tracer) c.tracer = nil } if c.meter != nil { if meter, ok := c.meter.meter.(*LoggingMeter); ok { meter.close() } c.meter = nil } return overallErr } func (c *Cluster) getDiagnosticsProvider() (diagnosticsProvider, error) { provider, err := c.connectionManager.getDiagnosticsProvider("") if err != nil { return nil, err } return provider, nil } func (c *Cluster) getQueryProvider() (queryProvider, error) { provider, err := c.connectionManager.getQueryProvider() if err != nil { return nil, err } return provider, nil } func (c *Cluster) getAnalyticsProvider() (analyticsProvider, error) { provider, err := c.connectionManager.getAnalyticsProvider() if err != nil { return nil, err } return provider, nil } func (c *Cluster) getSearchProvider() (searchProvider, error) { provider, err := c.connectionManager.getSearchProvider() if err != nil { return nil, err } return provider, nil } func (c *Cluster) getHTTPProvider() (httpProvider, error) { provider, err := c.connectionManager.getHTTPProvider("") if err != nil { return nil, err } return provider, nil } // Users returns a UserManager for managing users. func (c *Cluster) Users() *UserManager { return &UserManager{ provider: c, tracer: c.tracer, meter: c.meter, } } // Buckets returns a BucketManager for managing buckets. func (c *Cluster) Buckets() *BucketManager { return &BucketManager{ provider: c, tracer: c.tracer, meter: c.meter, } } // AnalyticsIndexes returns an AnalyticsIndexManager for managing analytics indexes. func (c *Cluster) AnalyticsIndexes() *AnalyticsIndexManager { return &AnalyticsIndexManager{ aProvider: c, mgmtProvider: c, globalTimeout: c.timeoutsConfig.ManagementTimeout, tracer: c.tracer, meter: c.meter, } } // QueryIndexes returns a QueryIndexManager for managing query indexes. func (c *Cluster) QueryIndexes() *QueryIndexManager { return &QueryIndexManager{ base: &baseQueryIndexManager{ provider: c, globalTimeout: c.timeoutsConfig.ManagementTimeout, tracer: c.tracer, meter: c.meter, }, } } // SearchIndexes returns a SearchIndexManager for managing search indexes. func (c *Cluster) SearchIndexes() *SearchIndexManager { return &SearchIndexManager{ mgmtProvider: c, tracer: c.tracer, meter: c.meter, } } // EventingFunctions returns a EventingFunctionManager for managing eventing functions. // UNCOMMITTED: This API may change in the future. func (c *Cluster) EventingFunctions() *EventingFunctionManager { return &EventingFunctionManager{ mgmtProvider: c, tracer: c.tracer, meter: c.meter, } } // Transactions returns a Transactions instance for performing transactions. func (c *Cluster) Transactions() *Transactions { return c.transactions } gocb-2.6.3/cluster_analyticsindexes.go000066400000000000000000001076261441755043100201130ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "github.com/google/uuid" "io/ioutil" "net/url" "strings" "time" ) // AnalyticsIndexManager provides methods for performing Couchbase Analytics index management. type AnalyticsIndexManager struct { aProvider analyticsIndexQueryProvider mgmtProvider mgmtProvider globalTimeout time.Duration tracer RequestTracer meter *meterWrapper } type analyticsIndexQueryProvider interface { AnalyticsQuery(statement string, opts *AnalyticsOptions) (*AnalyticsResult, error) } func (am *AnalyticsIndexManager) doAnalyticsQuery(q string, opts *AnalyticsOptions) ([][]byte, error) { if opts.Timeout == 0 { opts.Timeout = am.globalTimeout } result, err := am.aProvider.AnalyticsQuery(q, opts) if err != nil { return nil, err } var rows [][]byte for result.Next() { var row json.RawMessage err := result.Row(&row) if err != nil { logWarnf("management operation failed to read row: %s", err) } else { rows = append(rows, row) } } err = result.Err() if err != nil { return nil, err } return rows, nil } func (am *AnalyticsIndexManager) doMgmtRequest(ctx context.Context, req mgmtRequest) (*mgmtResponse, error) { resp, err := am.mgmtProvider.executeMgmtRequest(ctx, req) if err != nil { return nil, err } return resp, nil } type jsonAnalyticsDataset struct { DatasetName string `json:"DatasetName"` DataverseName string `json:"DataverseName"` LinkName string `json:"LinkName"` BucketName string `json:"BucketName"` } type jsonAnalyticsIndex struct { IndexName string `json:"IndexName"` DatasetName string `json:"DatasetName"` DataverseName string `json:"DataverseName"` IsPrimary bool `json:"IsPrimary"` } // AnalyticsDataset contains information about an analytics dataset. type AnalyticsDataset struct { Name string DataverseName string LinkName string BucketName string } func (ad *AnalyticsDataset) fromData(data jsonAnalyticsDataset) error { ad.Name = data.DatasetName ad.DataverseName = data.DataverseName ad.LinkName = data.LinkName ad.BucketName = data.BucketName return nil } // AnalyticsIndex contains information about an analytics index. type AnalyticsIndex struct { Name string DatasetName string DataverseName string IsPrimary bool } func (ai *AnalyticsIndex) fromData(data jsonAnalyticsIndex) error { ai.Name = data.IndexName ai.DatasetName = data.DatasetName ai.DataverseName = data.DataverseName ai.IsPrimary = data.IsPrimary return nil } // CreateAnalyticsDataverseOptions is the set of options available to the AnalyticsManager CreateDataverse operation. type CreateAnalyticsDataverseOptions struct { IgnoreIfExists bool Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } func (am *AnalyticsIndexManager) uncompoundName(dataverse string) string { dvPieces := strings.Split(dataverse, "/") return "`" + strings.Join(dvPieces, "`.`") + "`" } // CreateDataverse creates a new analytics dataset. func (am *AnalyticsIndexManager) CreateDataverse(dataverseName string, opts *CreateAnalyticsDataverseOptions) error { if opts == nil { opts = &CreateAnalyticsDataverseOptions{} } if dataverseName == "" { return invalidArgumentsError{ message: "dataset name cannot be empty", } } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_create_dataverse", start) var ignoreStr string if opts.IgnoreIfExists { ignoreStr = "IF NOT EXISTS" } q := fmt.Sprintf("CREATE DATAVERSE %s %s", am.uncompoundName(dataverseName), ignoreStr) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_create_dataverse", "management") defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, ClientContextID: uuid.New().String(), Context: opts.Context, }) if err != nil { return err } return nil } // DropAnalyticsDataverseOptions is the set of options available to the AnalyticsManager DropDataverse operation. type DropAnalyticsDataverseOptions struct { IgnoreIfNotExists bool Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropDataverse drops an analytics dataset. func (am *AnalyticsIndexManager) DropDataverse(dataverseName string, opts *DropAnalyticsDataverseOptions) error { if opts == nil { opts = &DropAnalyticsDataverseOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_drop_dataverse", start) var ignoreStr string if opts.IgnoreIfNotExists { ignoreStr = "IF EXISTS" } q := fmt.Sprintf("DROP DATAVERSE %s %s", am.uncompoundName(dataverseName), ignoreStr) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_drop_dataverse", "management") defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return err } // CreateAnalyticsDatasetOptions is the set of options available to the AnalyticsManager CreateDataset operation. type CreateAnalyticsDatasetOptions struct { IgnoreIfExists bool Condition string DataverseName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateDataset creates a new analytics dataset. func (am *AnalyticsIndexManager) CreateDataset(datasetName, bucketName string, opts *CreateAnalyticsDatasetOptions) error { if opts == nil { opts = &CreateAnalyticsDatasetOptions{} } if datasetName == "" { return invalidArgumentsError{ message: "dataset name cannot be empty", } } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_create_dataset", start) var ignoreStr string if opts.IgnoreIfExists { ignoreStr = "IF NOT EXISTS" } var where string if opts.Condition != "" { if !strings.HasPrefix(strings.ToUpper(opts.Condition), "WHERE") { where = "WHERE " } where += opts.Condition } if opts.DataverseName == "" { datasetName = fmt.Sprintf("`%s`", datasetName) } else { datasetName = fmt.Sprintf("%s.`%s`", am.uncompoundName(opts.DataverseName), datasetName) } q := fmt.Sprintf("CREATE DATASET %s %s ON `%s` %s", ignoreStr, datasetName, bucketName, where) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_create_dataset", "management") defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } // DropAnalyticsDatasetOptions is the set of options available to the AnalyticsManager DropDataset operation. type DropAnalyticsDatasetOptions struct { IgnoreIfNotExists bool DataverseName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropDataset drops an analytics dataset. func (am *AnalyticsIndexManager) DropDataset(datasetName string, opts *DropAnalyticsDatasetOptions) error { if opts == nil { opts = &DropAnalyticsDatasetOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_drop_dataset", start) var ignoreStr string if opts.IgnoreIfNotExists { ignoreStr = "IF EXISTS" } if opts.DataverseName == "" { datasetName = fmt.Sprintf("`%s`", datasetName) } else { datasetName = fmt.Sprintf("%s.`%s`", am.uncompoundName(opts.DataverseName), datasetName) } q := fmt.Sprintf("DROP DATASET %s %s", datasetName, ignoreStr) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_drop_dataset", "management") defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } // GetAllAnalyticsDatasetsOptions is the set of options available to the AnalyticsManager GetAllDatasets operation. type GetAllAnalyticsDatasetsOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllDatasets gets all analytics datasets. func (am *AnalyticsIndexManager) GetAllDatasets(opts *GetAllAnalyticsDatasetsOptions) ([]AnalyticsDataset, error) { if opts == nil { opts = &GetAllAnalyticsDatasetsOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_get_all_datasets", start) q := "SELECT d.* FROM Metadata.`Dataset` d WHERE d.DataverseName <> \"Metadata\"" span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_get_all_datasets", "management") span.SetAttribute("db.statement", q) defer span.End() rows, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return nil, err } datasets := make([]AnalyticsDataset, len(rows)) for rowIdx, row := range rows { var datasetData jsonAnalyticsDataset err := json.Unmarshal(row, &datasetData) if err != nil { return nil, err } err = datasets[rowIdx].fromData(datasetData) if err != nil { return nil, err } } return datasets, nil } // CreateAnalyticsIndexOptions is the set of options available to the AnalyticsManager CreateIndex operation. type CreateAnalyticsIndexOptions struct { IgnoreIfExists bool DataverseName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateIndex creates a new analytics dataset. func (am *AnalyticsIndexManager) CreateIndex(datasetName, indexName string, fields map[string]string, opts *CreateAnalyticsIndexOptions) error { if opts == nil { opts = &CreateAnalyticsIndexOptions{} } if indexName == "" { return invalidArgumentsError{ message: "index name cannot be empty", } } if len(fields) <= 0 { return invalidArgumentsError{ message: "you must specify at least one field to index", } } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_create_index", start) var ignoreStr string if opts.IgnoreIfExists { ignoreStr = "IF NOT EXISTS" } var indexFields []string for name, typ := range fields { indexFields = append(indexFields, name+":"+typ) } if opts.DataverseName == "" { datasetName = fmt.Sprintf("`%s`", datasetName) } else { datasetName = fmt.Sprintf("%s.`%s`", am.uncompoundName(opts.DataverseName), datasetName) } q := fmt.Sprintf("CREATE INDEX `%s` %s ON %s (%s)", indexName, ignoreStr, datasetName, strings.Join(indexFields, ",")) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_create_index", "management") defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } // DropAnalyticsIndexOptions is the set of options available to the AnalyticsManager DropIndex operation. type DropAnalyticsIndexOptions struct { IgnoreIfNotExists bool DataverseName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropIndex drops an analytics index. func (am *AnalyticsIndexManager) DropIndex(datasetName, indexName string, opts *DropAnalyticsIndexOptions) error { if opts == nil { opts = &DropAnalyticsIndexOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_drop_index", start) var ignoreStr string if opts.IgnoreIfNotExists { ignoreStr = "IF EXISTS" } if opts.DataverseName == "" { datasetName = fmt.Sprintf("`%s`", datasetName) } else { datasetName = fmt.Sprintf("%s.`%s`", am.uncompoundName(opts.DataverseName), datasetName) } q := fmt.Sprintf("DROP INDEX %s.%s %s", datasetName, indexName, ignoreStr) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_drop_index", "management") span.SetAttribute("db.statement", q) defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } // GetAllAnalyticsIndexesOptions is the set of options available to the AnalyticsManager GetAllIndexes operation. type GetAllAnalyticsIndexesOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllIndexes gets all analytics indexes. func (am *AnalyticsIndexManager) GetAllIndexes(opts *GetAllAnalyticsIndexesOptions) ([]AnalyticsIndex, error) { if opts == nil { opts = &GetAllAnalyticsIndexesOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_get_all_indexes", start) q := "SELECT d.* FROM Metadata.`Index` d WHERE d.DataverseName <> \"Metadata\"" span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_get_all_indexes", "management") defer span.End() rows, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return nil, err } indexes := make([]AnalyticsIndex, len(rows)) for rowIdx, row := range rows { var indexData jsonAnalyticsIndex err := json.Unmarshal(row, &indexData) if err != nil { return nil, err } err = indexes[rowIdx].fromData(indexData) if err != nil { return nil, err } } return indexes, nil } // ConnectAnalyticsLinkOptions is the set of options available to the AnalyticsManager ConnectLink operation. type ConnectAnalyticsLinkOptions struct { LinkName string DataverseName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // ConnectLink connects an analytics link. func (am *AnalyticsIndexManager) ConnectLink(opts *ConnectAnalyticsLinkOptions) error { if opts == nil { opts = &ConnectAnalyticsLinkOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_connect_link", start) linkName := opts.LinkName if linkName == "" { linkName = "Local" } if opts.DataverseName != "" { linkName = fmt.Sprintf("%s.`%s`", am.uncompoundName(opts.DataverseName), linkName) } q := fmt.Sprintf("CONNECT LINK %s", linkName) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_connect_link", "management") span.SetAttribute("db.statement", q) defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } // DisconnectAnalyticsLinkOptions is the set of options available to the AnalyticsManager DisconnectLink operation. type DisconnectAnalyticsLinkOptions struct { LinkName string DataverseName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DisconnectLink disconnects an analytics link. func (am *AnalyticsIndexManager) DisconnectLink(opts *DisconnectAnalyticsLinkOptions) error { if opts == nil { opts = &DisconnectAnalyticsLinkOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_disconnect_link", start) linkName := opts.LinkName if linkName == "" { linkName = "Local" } if opts.DataverseName != "" { linkName = fmt.Sprintf("%s.`%s`", am.uncompoundName(opts.DataverseName), linkName) } q := fmt.Sprintf("DISCONNECT LINK %s", linkName) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_disconnect_link", "management") defer span.End() _, err := am.doAnalyticsQuery(q, &AnalyticsOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: span, Context: opts.Context, }) if err != nil { return err } return nil } // GetPendingMutationsAnalyticsOptions is the set of options available to the user manager GetPendingMutations operation. type GetPendingMutationsAnalyticsOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetPendingMutations returns the number of pending mutations for all indexes in the form of dataverse.dataset:mutations. func (am *AnalyticsIndexManager) GetPendingMutations(opts *GetPendingMutationsAnalyticsOptions) (map[string]map[string]int, error) { if opts == nil { opts = &GetPendingMutationsAnalyticsOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_get_pending_mutations", start) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_get_pending_mutations", "management") span.SetAttribute("db.operation", "GET /analytics/node/agg/stats/remaining") defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = am.globalTimeout } req := mgmtRequest{ Service: ServiceTypeAnalytics, Method: "GET", Path: "/analytics/node/agg/stats/remaining", IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: timeout, parentSpanCtx: span.Context(), } resp, err := am.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, makeMgmtBadStatusError("failed to get pending mutations", &req, resp) } pending := make(map[string]map[string]int) jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&pending) if err != nil { return nil, err } err = resp.Body.Close() if err != nil { logDebugf("Failed to close socket (%s)", err) } return pending, nil } // AnalyticsLink describes an external or remote analytics link, used to access data external to the cluster. type AnalyticsLink interface { // Name returns the name of this link. Name() string // DataverseName returns the name of the dataverse that this link belongs to. DataverseName() string // FormEncode encodes the link into a form data representation, to be sent as the body of a CreateLink or ReplaceLink // request. FormEncode() ([]byte, error) // Validate is used by CreateLink and ReplaceLink to ensure that the link is valid. Validate() error // LinkType returns the type of analytics type this link is. LinkType() AnalyticsLinkType } // NewCouchbaseRemoteAnalyticsLinkOptions are the options available when creating a new CouchbaseRemoteAnalyticsLink. type NewCouchbaseRemoteAnalyticsLinkOptions struct { Encryption CouchbaseRemoteAnalyticsEncryptionSettings Username string Password string } // NewCouchbaseRemoteAnalyticsLink creates a new CouchbaseRemoteAnalyticsLink. // Scope is the analytics scope in the form of "bucket/scope". func NewCouchbaseRemoteAnalyticsLink(linkName, hostname, dataverseName string, opts *NewCouchbaseRemoteAnalyticsLinkOptions) *CouchbaseRemoteAnalyticsLink { if opts == nil { opts = &NewCouchbaseRemoteAnalyticsLinkOptions{} } return &CouchbaseRemoteAnalyticsLink{ Dataverse: dataverseName, LinkName: linkName, Hostname: hostname, Encryption: opts.Encryption, Username: opts.Username, Password: opts.Password, } } // NewS3ExternalAnalyticsLinkOptions are the options available when creating a new S3ExternalAnalyticsLink. type NewS3ExternalAnalyticsLinkOptions struct { SessionToken string ServiceEndpoint string } // NewS3ExternalAnalyticsLink creates a new S3ExternalAnalyticsLink with the scope field populated. // Scope is the analytics scope in the form of "bucket/scope". func NewS3ExternalAnalyticsLink(linkName, dataverseName, accessKeyID, secretAccessKey, region string, opts *NewS3ExternalAnalyticsLinkOptions) *S3ExternalAnalyticsLink { if opts == nil { opts = &NewS3ExternalAnalyticsLinkOptions{} } return &S3ExternalAnalyticsLink{ Dataverse: dataverseName, LinkName: linkName, AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, SessionToken: opts.SessionToken, Region: region, ServiceEndpoint: opts.ServiceEndpoint, } } // NewAzureBlobExternalAnalyticsLinkOptions are the options available when creating a new AzureBlobExternalAnalyticsLink. // VOLATILE: This API is subject to change at any time. type NewAzureBlobExternalAnalyticsLinkOptions struct { ConnectionString string AccountName string AccountKey string SharedAccessSignature string BlobEndpoint string EndpointSuffix string } // NewAzureBlobExternalAnalyticsLink creates a new AzureBlobExternalAnalyticsLink. // VOLATILE: This API is subject to change at any time. func NewAzureBlobExternalAnalyticsLink(linkName, dataverseName string, opts *NewAzureBlobExternalAnalyticsLinkOptions) *AzureBlobExternalAnalyticsLink { if opts == nil { opts = &NewAzureBlobExternalAnalyticsLinkOptions{} } return &AzureBlobExternalAnalyticsLink{ Dataverse: dataverseName, LinkName: linkName, ConnectionString: opts.ConnectionString, AccountName: opts.AccountName, AccountKey: opts.AccountKey, SharedAccessSignature: opts.SharedAccessSignature, BlobEndpoint: opts.BlobEndpoint, EndpointSuffix: opts.EndpointSuffix, } } // CreateAnalyticsLinkOptions is the set of options available to the analytics manager CreateLink // function. type CreateAnalyticsLinkOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateLink creates an analytics link. func (am *AnalyticsIndexManager) CreateLink(link AnalyticsLink, opts *CreateAnalyticsLinkOptions) error { if opts == nil { opts = &CreateAnalyticsLinkOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_create_link", start) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_create_link", "management") defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = am.globalTimeout } if err := link.Validate(); err != nil { return err } endpoint := am.endpointFromLink(link) span.SetAttribute("db.operation", "POST "+endpoint) eSpan := createSpan(am.tracer, span, "request_encoding", "") data, err := link.FormEncode() eSpan.End() if err != nil { return err } req := mgmtRequest{ Service: ServiceTypeAnalytics, Method: "POST", Path: endpoint, RetryStrategy: opts.RetryStrategy, Timeout: timeout, parentSpanCtx: span.Context(), Body: data, ContentType: "application/x-www-form-urlencoded", } resp, err := am.doMgmtRequest(opts.Context, req) if err != nil { return err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return am.tryParseLinkErrorMessage(&req, resp) } return nil } // ReplaceAnalyticsLinkOptions is the set of options available to the analytics manager ReplaceLink // function. type ReplaceAnalyticsLinkOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // ReplaceLink modifies an existing analytics link. func (am *AnalyticsIndexManager) ReplaceLink(link AnalyticsLink, opts *ReplaceAnalyticsLinkOptions) error { if opts == nil { opts = &ReplaceAnalyticsLinkOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_replace_link", start) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_replace_link", "management") defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = am.globalTimeout } if err := link.Validate(); err != nil { return err } endpoint := am.endpointFromLink(link) span.SetAttribute("db.operation", "PUT "+endpoint) eSpan := createSpan(am.tracer, span, "request_encoding", "") data, err := link.FormEncode() eSpan.End() if err != nil { return err } req := mgmtRequest{ Service: ServiceTypeAnalytics, Method: "PUT", Path: endpoint, RetryStrategy: opts.RetryStrategy, Timeout: timeout, parentSpanCtx: span.Context(), Body: data, ContentType: "application/x-www-form-urlencoded", } resp, err := am.doMgmtRequest(opts.Context, req) if err != nil { return err } if resp.StatusCode != 200 { return am.tryParseLinkErrorMessage(&req, resp) } return nil } // DropAnalyticsLinkOptions is the set of options available to the analytics manager DropLink // function. type DropAnalyticsLinkOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropLink removes an existing external analytics link from specified scope. // dataverseName can be given in the form of "namepart" or "namepart1/namepart2". // Only available against Couchbase Server 7.0+. func (am *AnalyticsIndexManager) DropLink(linkName, dataverseName string, opts *DropAnalyticsLinkOptions) error { if opts == nil { opts = &DropAnalyticsLinkOptions{} } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_drop_link", start) span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_drop_link", "management") defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = am.globalTimeout } var payload []byte var endpoint string if strings.Contains(dataverseName, "/") { endpoint = fmt.Sprintf("/analytics/link/%s/%s", url.PathEscape(dataverseName), linkName) } else { endpoint = "/analytics/link" values := url.Values{} values.Add("dataverse", dataverseName) values.Add("name", linkName) eSpan := createSpan(am.tracer, span, spanNameRequestEncoding, "management") payload = []byte(values.Encode()) eSpan.End() } span.SetAttribute("db.operation", "DELETE "+endpoint) req := mgmtRequest{ Service: ServiceTypeAnalytics, Method: "DELETE", Path: endpoint, RetryStrategy: opts.RetryStrategy, Timeout: timeout, parentSpanCtx: span.Context(), ContentType: "application/x-www-form-urlencoded", Body: payload, } resp, err := am.doMgmtRequest(opts.Context, req) if err != nil { return err } if resp.StatusCode != 200 { return am.tryParseLinkErrorMessage(&req, resp) } return nil } // GetAnalyticsLinksOptions are the options available to the AnalyticsManager GetLinks function. type GetAnalyticsLinksOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Dataverse restricts the results to a given dataverse, can be given in the form of "namepart" or "namepart1/namepart2". Dataverse string // LinkType restricts the results to the given link type. LinkType AnalyticsLinkType // Name restricts the results to the link with the specified name. // If set then `Scope` must also be set. Name string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetLinks retrieves all external or remote analytics links. func (am *AnalyticsIndexManager) GetLinks(opts *GetAnalyticsLinksOptions) ([]AnalyticsLink, error) { if opts == nil { opts = &GetAnalyticsLinksOptions{} } if opts.Name != "" && opts.Dataverse == "" { return nil, makeInvalidArgumentsError("when name is set then dataverse must also be set") } start := time.Now() defer am.meter.ValueRecord(meterValueServiceManagement, "manager_analytics_get_all_links", start) timeout := opts.Timeout if timeout == 0 { timeout = am.globalTimeout } var querystring []string var endpoint string if strings.Contains(opts.Dataverse, "/") { endpoint = fmt.Sprintf("/analytics/link/%s", url.PathEscape(opts.Dataverse)) if opts.Name != "" { endpoint = fmt.Sprintf("%s/%s", endpoint, opts.Name) } if opts.LinkType != "" { querystring = append(querystring, fmt.Sprintf("type=%s", opts.LinkType)) } } else { endpoint = "/analytics/link" if opts.Dataverse != "" { querystring = append(querystring, "dataverse="+opts.Dataverse) if opts.Name != "" { querystring = append(querystring, "name="+opts.Name) } } if opts.LinkType != "" { querystring = append(querystring, fmt.Sprintf("type=%s", opts.LinkType)) } } if len(querystring) > 0 { endpoint = endpoint + "?" + strings.Join(querystring, "&") } span := createSpan(am.tracer, opts.ParentSpan, "manager_analytics_get_all_links", "management") span.SetAttribute("db.operation", "GET "+endpoint) defer span.End() req := mgmtRequest{ Service: ServiceTypeAnalytics, Method: "GET", Path: endpoint, RetryStrategy: opts.RetryStrategy, Timeout: timeout, parentSpanCtx: span.Context(), IsIdempotent: true, } resp, err := am.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, am.tryParseLinkErrorMessage(&req, resp) } var jsonLinks []map[string]interface{} jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&jsonLinks) if err != nil { return nil, err } var links []AnalyticsLink for _, jsonLink := range jsonLinks { linkType, ok := jsonLink["type"] if !ok { logWarnf("External analytics link missing type field, skipping") continue } linkTypeStr, ok := linkType.(string) if !ok { logWarnf("External analytics link type field not a string, skipping") continue } link := am.linkFromJSON(AnalyticsLinkType(linkTypeStr), jsonLink) if link == nil { logWarnf("External analytics link type %s unknown, skipping", linkTypeStr) continue } links = append(links, link) } return links, nil } func (am *AnalyticsIndexManager) fieldFromJSONMapAsString(name string, json map[string]interface{}) string { field, ok := json[name] if !ok { return "" } strField, ok := field.(string) if !ok { return "" } return strField } func (am *AnalyticsIndexManager) endpointFromLink(link AnalyticsLink) string { var endpoint string switch l := link.(type) { case *CouchbaseRemoteAnalyticsLink: if strings.Contains(l.Dataverse, "/") { endpoint = fmt.Sprintf("/analytics/link/%s/%s", url.PathEscape(l.Dataverse), l.LinkName) } else { endpoint = "/analytics/link" } case *S3ExternalAnalyticsLink: if strings.Contains(l.Dataverse, "/") { endpoint = fmt.Sprintf("/analytics/link/%s/%s", url.PathEscape(l.Dataverse), l.LinkName) } else { endpoint = "/analytics/link" } case *AzureBlobExternalAnalyticsLink: endpoint = fmt.Sprintf("/analytics/link/%s/%s", url.PathEscape(l.Dataverse), l.LinkName) default: endpoint = "/analytics/link" } return endpoint } func (am *AnalyticsIndexManager) tryParseLinkErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read bucket manager response body: %s", err) return nil } if strings.Contains(strings.ToLower(string(b)), "24055") { return makeGenericMgmtError(ErrAnalyticsLinkExists, req, resp, string(b)) } if strings.Contains(strings.ToLower(string(b)), "24034") { return makeGenericMgmtError(ErrDataverseNotFound, req, resp, string(b)) } return makeGenericMgmtError(errors.New(string(b)), req, resp, string(b)) } func (am *AnalyticsIndexManager) linkFromJSON(linkType AnalyticsLinkType, jsonLink map[string]interface{}) AnalyticsLink { dataverse := am.fieldFromJSONMapAsString("dataverse", jsonLink) if dataverse == "" { dataverse = am.fieldFromJSONMapAsString("scope", jsonLink) } switch linkType { case AnalyticsLinkTypeCouchbaseRemote: encryptionLevel := am.fieldFromJSONMapAsString("encryption", jsonLink) return &CouchbaseRemoteAnalyticsLink{ Dataverse: dataverse, LinkName: am.fieldFromJSONMapAsString("name", jsonLink), Hostname: am.fieldFromJSONMapAsString("activeHostname", jsonLink), Encryption: CouchbaseRemoteAnalyticsEncryptionSettings{ EncryptionLevel: analyticsEncryptionLevelFromString(encryptionLevel), Certificate: []byte(am.fieldFromJSONMapAsString("certificate", jsonLink)), ClientCertificate: []byte(am.fieldFromJSONMapAsString("clientCertificate", jsonLink)), }, Username: am.fieldFromJSONMapAsString("username", jsonLink), } case AnalyticsLinkTypeS3External: return &S3ExternalAnalyticsLink{ Dataverse: dataverse, LinkName: am.fieldFromJSONMapAsString("name", jsonLink), AccessKeyID: am.fieldFromJSONMapAsString("accessKeyId", jsonLink), Region: am.fieldFromJSONMapAsString("region", jsonLink), ServiceEndpoint: am.fieldFromJSONMapAsString("serviceEndpoint", jsonLink), } case AnalyticsLinkTypeAzureExternal: return &AzureBlobExternalAnalyticsLink{ Dataverse: dataverse, LinkName: am.fieldFromJSONMapAsString("name", jsonLink), AccountName: am.fieldFromJSONMapAsString("accountName", jsonLink), BlobEndpoint: am.fieldFromJSONMapAsString("blobEndpoint", jsonLink), EndpointSuffix: am.fieldFromJSONMapAsString("endpointSuffix", jsonLink), } default: return nil } } gocb-2.6.3/cluster_analyticsindexes_test.go000066400000000000000000000755301441755043100211500ustar00rootroot00000000000000package gocb import ( "errors" "net/url" ) func (suite *IntegrationTestSuite) TestAnalyticsIndexesCrud() { suite.skipIfUnsupported(AnalyticsIndexFeature) mgr := globalCluster.AnalyticsIndexes() err := mgr.CreateDataverse("testaverse", nil) if err != nil { suite.T().Fatalf("Expected CreateDataverse to not error %v", err) } err = mgr.CreateDataverse("testaverse", &CreateAnalyticsDataverseOptions{ IgnoreIfExists: true, }) if err != nil { suite.T().Fatalf("Expected CreateDataverse to not error %v", err) } err = mgr.CreateDataverse("testaverse", nil) if err == nil { suite.T().Fatalf("Expected CreateDataverse to error") } if !errors.Is(err, ErrDataverseExists) { suite.T().Fatalf("Expected error to be dataverse already exists but was %v", err) } err = mgr.CreateDataset("testaset", globalBucket.Name(), &CreateAnalyticsDatasetOptions{ DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected CreateDataset to not error %v", err) } err = mgr.CreateDataset("testaset", globalBucket.Name(), &CreateAnalyticsDatasetOptions{ IgnoreIfExists: true, DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected CreateDataset to not error %v", err) } err = mgr.CreateDataset("testaset", globalBucket.Name(), &CreateAnalyticsDatasetOptions{ DataverseName: "testaverse", }) if err == nil { suite.T().Fatalf("Expected CreateDataverse to error") } if !errors.Is(err, ErrDatasetExists) { suite.T().Fatalf("Expected error to be dataset already exists but was %v", err) } err = mgr.CreateIndex("testaset", "testindex", map[string]string{ "testkey": "string", }, &CreateAnalyticsIndexOptions{ IgnoreIfExists: true, DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected CreateIndex to not error %v", err) } err = mgr.CreateIndex("testaset", "testindex", map[string]string{ "testkey": "string", }, &CreateAnalyticsIndexOptions{ IgnoreIfExists: true, DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected CreateIndex to not error %v", err) } err = mgr.CreateIndex("testaset", "testindex", map[string]string{ "testkey": "string", }, &CreateAnalyticsIndexOptions{ IgnoreIfExists: false, DataverseName: "testaverse", }) if err == nil { suite.T().Fatalf("Expected CreateIndex to error") } if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected error to be index already exists but was %v", err) } err = mgr.ConnectLink(nil) if err != nil { suite.T().Fatalf("Expected ConnectLink to not error %v", err) } datasets, err := mgr.GetAllDatasets(nil) if err != nil { suite.T().Fatalf("Expected GetAllDatasets to not error %v", err) } if len(datasets) == 0 { suite.T().Fatalf("Expected datasets length to be greater than 0") } indexes, err := mgr.GetAllIndexes(nil) if err != nil { suite.T().Fatalf("Expected GetAllIndexes to not error %v", err) } if len(indexes) == 0 { suite.T().Fatalf("Expected indexes length to be greater than 0") } if globalCluster.SupportsFeature(AnalyticsIndexPendingMutationsFeature) { _, err = mgr.GetPendingMutations(nil) if err != nil { suite.T().Fatalf("Expected GetPendingMutations to not error %v", err) } } err = mgr.DisconnectLink(nil) if err != nil { suite.T().Fatalf("Expected DisconnectLink to not error %v", err) } err = mgr.DropIndex("testaset", "testindex", &DropAnalyticsIndexOptions{ IgnoreIfNotExists: true, DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected DropIndex to not error %v", err) } err = mgr.DropIndex("testaset", "testindex", &DropAnalyticsIndexOptions{ IgnoreIfNotExists: true, DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected DropIndex to not error %v", err) } err = mgr.DropIndex("testaset", "testindex", &DropAnalyticsIndexOptions{ DataverseName: "testaverse", }) if err == nil { suite.T().Fatalf("Expected DropIndex to error") } if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected error to be index not found but was %v", err) } err = mgr.DropDataset("testaset", &DropAnalyticsDatasetOptions{ DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected DropDataset to not error %v", err) } err = mgr.DropDataset("testaset", &DropAnalyticsDatasetOptions{ IgnoreIfNotExists: true, DataverseName: "testaverse", }) if err != nil { suite.T().Fatalf("Expected DropDataset to not error %v", err) } err = mgr.DropDataset("testaset", &DropAnalyticsDatasetOptions{ DataverseName: "testaverse", }) if err == nil { suite.T().Fatalf("Expected DropDataset to error") } if !errors.Is(err, ErrDatasetNotFound) { suite.T().Fatalf("Expected error to be dataset not found but was %v", err) } err = mgr.DropDataverse("testaverse", nil) if err != nil { suite.T().Fatalf("Expected DropDataverse to not error %v", err) } err = mgr.DropDataverse("testaverse", &DropAnalyticsDataverseOptions{ IgnoreIfNotExists: true, }) if err != nil { suite.T().Fatalf("Expected DropDataverse to not error %v", err) } err = mgr.DropDataverse("testaverse", nil) if err == nil { suite.T().Fatalf("Expected DropDataverse to error") } if !errors.Is(err, ErrDataverseNotFound) { suite.T().Fatalf("Expected error to be dataverse not found but was %v", err) } spans := 22 if globalCluster.SupportsFeature(AnalyticsIndexPendingMutationsFeature) { spans = 23 } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(spans, len(nilParents)) span := suite.RequireQueryMgmtOpSpan(nilParents[0], "manager_analytics_create_dataverse", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[3], "manager_analytics_create_dataset", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[6], "manager_analytics_create_index", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[9], "manager_analytics_connect_link", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[10], "manager_analytics_get_all_datasets", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[11], "manager_analytics_get_all_indexes", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) offset := 0 if globalCluster.SupportsFeature(AnalyticsIndexPendingMutationsFeature) { offset = 1 suite.AssertHTTPOpSpan(nilParents[12], "manager_analytics_get_pending_mutations", HTTPOpSpanExpectations{ operationID: "GET /analytics/node/agg/stats/remaining", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) } span = suite.RequireQueryMgmtOpSpan(nilParents[12+offset], "manager_analytics_disconnect_link", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[13+offset], "manager_analytics_drop_index", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[16+offset], "manager_analytics_drop_dataset", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[19+offset], "manager_analytics_drop_dataverse", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) numResponses := 22 suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_dataverse"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_dataset"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_index"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_connect_link"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_get_all_datasets"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_get_all_indexes"), 1, false) if globalCluster.SupportsFeature(AnalyticsIndexPendingMutationsFeature) { numResponses++ suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_get_pending_mutations"), 1, false) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_disconnect_link"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_dataverse"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_dataset"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_index"), 3, false) } func (suite *IntegrationTestSuite) TestAnalyticsIndexesCrudCompoundNames() { suite.skipIfUnsupported(AnalyticsIndexFeature) suite.skipIfUnsupported(AnalyticsIndexLinksScopesFeature) mgr := globalCluster.AnalyticsIndexes() dataverse := "namepart1/namepart2" err := mgr.CreateDataverse(dataverse, nil) if err != nil { suite.T().Fatalf("Expected CreateDataverse to not error %v", err) } err = mgr.CreateDataset("testaset", globalBucket.Name(), &CreateAnalyticsDatasetOptions{ DataverseName: dataverse, }) if err != nil { suite.T().Fatalf("Expected CreateDataset to not error %v", err) } err = mgr.CreateIndex("testaset", "testindex", map[string]string{ "testkey": "string", }, &CreateAnalyticsIndexOptions{ IgnoreIfExists: true, DataverseName: dataverse, }) if err != nil { suite.T().Fatalf("Expected CreateIndex to not error %v", err) } err = mgr.ConnectLink(&ConnectAnalyticsLinkOptions{ DataverseName: dataverse, }) if err != nil { suite.T().Fatalf("Expected ConnectLink to not error %v", err) } err = mgr.DisconnectLink(&DisconnectAnalyticsLinkOptions{ DataverseName: dataverse, }) if err != nil { suite.T().Fatalf("Expected DisconnectLink to not error %v", err) } err = mgr.DropIndex("testaset", "testindex", &DropAnalyticsIndexOptions{ IgnoreIfNotExists: true, DataverseName: dataverse, }) if err != nil { suite.T().Fatalf("Expected DropIndex to not error %v", err) } err = mgr.DropDataset("testaset", &DropAnalyticsDatasetOptions{ DataverseName: dataverse, }) if err != nil { suite.T().Fatalf("Expected DropDataset to not error %v", err) } err = mgr.DropDataverse(dataverse, nil) if err != nil { suite.T().Fatalf("Expected DropDataverse to not error %v", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(8, len(nilParents)) span := suite.RequireQueryMgmtOpSpan(nilParents[0], "manager_analytics_create_dataverse", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[1], "manager_analytics_create_dataset", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[2], "manager_analytics_create_index", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[3], "manager_analytics_connect_link", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[4], "manager_analytics_disconnect_link", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[5], "manager_analytics_drop_index", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[6], "manager_analytics_drop_dataset", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) span = suite.RequireQueryMgmtOpSpan(nilParents[7], "manager_analytics_drop_dataverse", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, dispatchOperationID: "any", service: "analytics", statement: "any", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_dataverse"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_dataset"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_index"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_connect_link"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_disconnect_link"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_dataverse"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_dataset"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_index"), 1, false) } func (suite *IntegrationTestSuite) TestAnalyticsIndexesS3Links() { suite.skipIfUnsupported(AnalyticsIndexFeature) suite.skipIfUnsupported(AnalyticsIndexLinksFeature) mgr := globalCluster.AnalyticsIndexes() dataverse := "scopeslinkaverse" err := mgr.CreateDataverse(dataverse, &CreateAnalyticsDataverseOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) defer mgr.DropDataverse(dataverse, nil) link := NewS3ExternalAnalyticsLink("s3Link", dataverse, "accesskey", "secretKey", "us-east-1", nil) link2 := NewS3ExternalAnalyticsLink("s3Link2", dataverse, "2", "secretKey2", "us-east-1", &NewS3ExternalAnalyticsLinkOptions{ServiceEndpoint: "end"}) err = mgr.CreateLink(link, nil) suite.Require().Nil(err, err) err = mgr.CreateLink(link2, nil) suite.Require().Nil(err, err) err = mgr.CreateLink(link, nil) if !errors.Is(err, ErrAnalyticsLinkExists) { suite.T().Fatalf("Expected error to be link already exists but was %v", err) } links, err := mgr.GetLinks(nil) suite.Require().Nil(err, err) resultLink1 := &S3ExternalAnalyticsLink{ Dataverse: link.Dataverse, LinkName: link.Name(), AccessKeyID: link.AccessKeyID, Region: link.Region, ServiceEndpoint: link.ServiceEndpoint, } resultLink2 := &S3ExternalAnalyticsLink{ Dataverse: link2.Dataverse, LinkName: link2.Name(), AccessKeyID: link2.AccessKeyID, Region: link2.Region, ServiceEndpoint: link2.ServiceEndpoint, } suite.Require().Len(links, 2) suite.Assert().Contains(links, resultLink1) suite.Assert().Contains(links, resultLink2) links, err = mgr.GetLinks(&GetAnalyticsLinksOptions{ Dataverse: dataverse, Name: link.Name(), LinkType: AnalyticsLinkTypeS3External, }) suite.Require().Nil(err, err) suite.Require().Len(links, 1) suite.Assert().Contains(links, resultLink1) rLink := NewS3ExternalAnalyticsLink("s3Link", dataverse, "accesskey2", "secretKey", "us-east-1", nil) err = mgr.ReplaceLink(rLink, nil) suite.Require().Nil(err, err) links, err = mgr.GetLinks(nil) suite.Require().Nil(err, err) suite.Require().Len(links, 2) err = mgr.DropLink("s3Link", dataverse, nil) suite.Require().Nil(err, err) err = mgr.DropLink("s3Link2", dataverse, nil) suite.Require().Nil(err, err) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(10, len(nilParents)) span := suite.RequireQueryMgmtOpSpan(nilParents[0], "manager_analytics_create_dataverse", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) suite.AssertHTTPOpSpan(nilParents[1], "manager_analytics_create_link", HTTPOpSpanExpectations{ operationID: "POST /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[2], "manager_analytics_create_link", HTTPOpSpanExpectations{ operationID: "POST /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[3], "manager_analytics_create_link", HTTPOpSpanExpectations{ operationID: "POST /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[4], "manager_analytics_get_all_links", HTTPOpSpanExpectations{ operationID: "GET /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[5], "manager_analytics_get_all_links", HTTPOpSpanExpectations{ operationID: "GET /analytics/link?dataverse=" + dataverse + "&name=" + link.LinkName + "&type=s3", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[6], "manager_analytics_replace_link", HTTPOpSpanExpectations{ operationID: "PUT /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[7], "manager_analytics_get_all_links", HTTPOpSpanExpectations{ operationID: "GET /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[8], "manager_analytics_drop_link", HTTPOpSpanExpectations{ operationID: "DELETE /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[9], "manager_analytics_drop_link", HTTPOpSpanExpectations{ operationID: "DELETE /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_dataverse"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_link"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_replace_link"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_get_all_links"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_link"), 2, false) } func (suite *IntegrationTestSuite) TestAnalyticsIndexesS3LinksScopes() { suite.skipIfUnsupported(AnalyticsIndexFeature) suite.skipIfUnsupported(AnalyticsIndexLinksFeature) suite.skipIfUnsupported(AnalyticsIndexLinksScopesFeature) mgr := globalCluster.AnalyticsIndexes() dataverse := globalBucket.Name() + "/" + globalScope.Name() err := mgr.CreateDataverse(dataverse, &CreateAnalyticsDataverseOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) defer mgr.DropDataverse(dataverse, nil) link := NewS3ExternalAnalyticsLink("s3LinkScope", dataverse, "accesskey", "secretKey", "us-east-1", nil) link2 := NewS3ExternalAnalyticsLink("s3LinkScope2", dataverse, "2", "secretKey2", "us-east-1", &NewS3ExternalAnalyticsLinkOptions{ServiceEndpoint: "end"}) err = mgr.CreateLink(link, nil) suite.Require().Nil(err, err) err = mgr.CreateLink(link2, nil) suite.Require().Nil(err, err) err = mgr.CreateLink(link, nil) if !errors.Is(err, ErrAnalyticsLinkExists) { suite.T().Fatalf("Expected error to be link already exists but was %v", err) } links, err := mgr.GetLinks(nil) suite.Require().Nil(err, err) resultLink1 := &S3ExternalAnalyticsLink{ Dataverse: link.Dataverse, LinkName: link.LinkName, AccessKeyID: link.AccessKeyID, Region: link.Region, ServiceEndpoint: link.ServiceEndpoint, } resultLink2 := &S3ExternalAnalyticsLink{ Dataverse: link2.Dataverse, LinkName: link2.LinkName, AccessKeyID: link2.AccessKeyID, Region: link2.Region, ServiceEndpoint: link2.ServiceEndpoint, } suite.Require().Len(links, 2) suite.Assert().Contains(links, resultLink1) suite.Assert().Contains(links, resultLink2) links, err = mgr.GetLinks(&GetAnalyticsLinksOptions{ Dataverse: dataverse, Name: link.Name(), LinkType: AnalyticsLinkTypeS3External, }) suite.Require().Nil(err, err) suite.Require().Len(links, 1) suite.Assert().Contains(links, resultLink1) rLink := NewS3ExternalAnalyticsLink("s3LinkScope", dataverse, "accesskey2", "secretKey", "us-east-1", nil) err = mgr.ReplaceLink(rLink, nil) suite.Require().Nil(err, err) links, err = mgr.GetLinks(nil) suite.Require().Nil(err, err) suite.Require().Len(links, 2) err = mgr.DropLink("s3LinkScope", dataverse, nil) suite.Require().Nil(err, err) err = mgr.DropLink("s3LinkScope2", dataverse, nil) suite.Require().Nil(err, err) escapedScope := url.PathEscape(dataverse) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(10, len(nilParents)) span := suite.RequireQueryMgmtOpSpan(nilParents[0], "manager_analytics_create_dataverse", "analytics") suite.AssertHTTPOpSpan(span, "analytics", HTTPOpSpanExpectations{ numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, dispatchOperationID: "any", service: "analytics", statement: "any", }) suite.AssertHTTPOpSpan(nilParents[1], "manager_analytics_create_link", HTTPOpSpanExpectations{ operationID: "POST /analytics/link/" + escapedScope + "/s3LinkScope", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[2], "manager_analytics_create_link", HTTPOpSpanExpectations{ operationID: "POST /analytics/link/" + escapedScope + "/s3LinkScope2", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[3], "manager_analytics_create_link", HTTPOpSpanExpectations{ operationID: "POST /analytics/link/" + escapedScope + "/s3LinkScope", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[4], "manager_analytics_get_all_links", HTTPOpSpanExpectations{ operationID: "GET /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[5], "manager_analytics_get_all_links", HTTPOpSpanExpectations{ operationID: "GET /analytics/link/" + escapedScope + "/s3LinkScope" + "?type=s3", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[6], "manager_analytics_replace_link", HTTPOpSpanExpectations{ operationID: "PUT /analytics/link/" + escapedScope + "/s3LinkScope", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "management", }) suite.AssertHTTPOpSpan(nilParents[7], "manager_analytics_get_all_links", HTTPOpSpanExpectations{ operationID: "GET /analytics/link", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[8], "manager_analytics_drop_link", HTTPOpSpanExpectations{ operationID: "DELETE /analytics/link/" + escapedScope + "/s3LinkScope", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertHTTPOpSpan(nilParents[9], "manager_analytics_drop_link", HTTPOpSpanExpectations{ operationID: "DELETE /analytics/link/" + escapedScope + "/s3LinkScope2", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: false, service: "management", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_dataverse"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_create_link"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_replace_link"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_get_all_links"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_analytics_drop_link"), 2, false) } func (suite *UnitTestSuite) TestAnalyticsIndexesCouchbaseLinksFormEncode() { link := NewCouchbaseRemoteAnalyticsLink("link", "host", "scope", &NewCouchbaseRemoteAnalyticsLinkOptions{ Username: "username", Password: "password", }) body, err := link.FormEncode() suite.Require().Nil(err, err) data := string(body) q, err := url.ParseQuery(data) suite.Require().Nil(err) suite.Assert().Equal("host", q.Get("hostname")) suite.Assert().Equal(string(AnalyticsLinkTypeCouchbaseRemote), q.Get("type")) suite.Assert().Equal("none", q.Get("encryption")) suite.Assert().Equal("username", q.Get("username")) suite.Assert().Equal("password", q.Get("password")) link = NewCouchbaseRemoteAnalyticsLink("link", "host", "scope", &NewCouchbaseRemoteAnalyticsLinkOptions{ Encryption: CouchbaseRemoteAnalyticsEncryptionSettings{ EncryptionLevel: AnalyticsEncryptionLevelFull, Certificate: []byte("certificate"), ClientCertificate: []byte("clientcertificate"), ClientKey: []byte("clientkey"), }, }) body, err = link.FormEncode() suite.Require().Nil(err, err) data = string(body) q, err = url.ParseQuery(data) suite.Require().Nil(err) suite.Assert().Equal("host", q.Get("hostname")) suite.Assert().Equal(string(AnalyticsLinkTypeCouchbaseRemote), q.Get("type")) suite.Assert().Equal("full", q.Get("encryption")) suite.Assert().Equal("certificate", q.Get("certificate")) suite.Assert().Equal("clientcertificate", q.Get("clientCertificate")) suite.Assert().Equal("clientkey", q.Get("clientKey")) } gocb-2.6.3/cluster_analyticsquery.go000066400000000000000000000237121441755043100176120ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) type jsonAnalyticsMetrics struct { ElapsedTime string `json:"elapsedTime"` ExecutionTime string `json:"executionTime"` ResultCount uint64 `json:"resultCount"` ResultSize uint64 `json:"resultSize"` MutationCount uint64 `json:"mutationCount,omitempty"` SortCount uint64 `json:"sortCount,omitempty"` ErrorCount uint64 `json:"errorCount,omitempty"` WarningCount uint64 `json:"warningCount,omitempty"` ProcessedObjects uint64 `json:"processedObjects,omitempty"` } type jsonAnalyticsWarning struct { Code uint32 `json:"code"` Message string `json:"msg"` } type jsonAnalyticsResponse struct { RequestID string `json:"requestID"` ClientContextID string `json:"clientContextID"` Status string `json:"status"` Warnings []jsonAnalyticsWarning `json:"warnings"` Metrics jsonAnalyticsMetrics `json:"metrics"` Signature interface{} `json:"signature"` } // AnalyticsMetrics encapsulates various metrics gathered during a queries execution. type AnalyticsMetrics struct { ElapsedTime time.Duration ExecutionTime time.Duration ResultCount uint64 ResultSize uint64 MutationCount uint64 SortCount uint64 ErrorCount uint64 WarningCount uint64 ProcessedObjects uint64 } func (metrics *AnalyticsMetrics) fromData(data jsonAnalyticsMetrics) error { elapsedTime, err := time.ParseDuration(data.ElapsedTime) if err != nil { logDebugf("Failed to parse query metrics elapsed time: %s", err) } executionTime, err := time.ParseDuration(data.ExecutionTime) if err != nil { logDebugf("Failed to parse query metrics execution time: %s", err) } metrics.ElapsedTime = elapsedTime metrics.ExecutionTime = executionTime metrics.ResultCount = data.ResultCount metrics.ResultSize = data.ResultSize metrics.MutationCount = data.MutationCount metrics.SortCount = data.SortCount metrics.ErrorCount = data.ErrorCount metrics.WarningCount = data.WarningCount metrics.ProcessedObjects = data.ProcessedObjects return nil } // AnalyticsWarning encapsulates any warnings returned by a query. type AnalyticsWarning struct { Code uint32 Message string } func (warning *AnalyticsWarning) fromData(data jsonAnalyticsWarning) error { warning.Code = data.Code warning.Message = data.Message return nil } // AnalyticsMetaData provides access to the meta-data properties of a query result. type AnalyticsMetaData struct { RequestID string ClientContextID string Metrics AnalyticsMetrics Signature interface{} Warnings []AnalyticsWarning } func (meta *AnalyticsMetaData) fromData(data jsonAnalyticsResponse) error { metrics := AnalyticsMetrics{} if err := metrics.fromData(data.Metrics); err != nil { return err } warnings := make([]AnalyticsWarning, len(data.Warnings)) for wIdx, jsonWarning := range data.Warnings { err := warnings[wIdx].fromData(jsonWarning) if err != nil { return err } } meta.RequestID = data.RequestID meta.ClientContextID = data.ClientContextID meta.Metrics = metrics meta.Signature = data.Signature meta.Warnings = warnings return nil } // AnalyticsResultRaw provides raw access to analytics query data. // VOLATILE: This API is subject to change at any time. type AnalyticsResultRaw struct { reader analyticsRowReader } // NextBytes returns the next row as bytes. func (arr *AnalyticsResultRaw) NextBytes() []byte { return arr.reader.NextRow() } // Err returns any errors that have occurred on the stream func (arr *AnalyticsResultRaw) Err() error { err := arr.reader.Err() if err != nil { return maybeEnhanceAnalyticsError(err) } return nil } // Close marks the results as closed, returning any errors that occurred during reading the results. func (arr *AnalyticsResultRaw) Close() error { err := arr.reader.Close() if err != nil { return maybeEnhanceAnalyticsError(err) } return nil } // MetaData returns any meta-data that was available from this query as bytes. func (arr *AnalyticsResultRaw) MetaData() ([]byte, error) { return arr.reader.MetaData() } // AnalyticsResult allows access to the results of a query. type AnalyticsResult struct { reader analyticsRowReader rowBytes []byte } func newAnalyticsResult(reader analyticsRowReader) *AnalyticsResult { return &AnalyticsResult{ reader: reader, } } type analyticsRowReader interface { NextRow() []byte Err() error MetaData() ([]byte, error) Close() error } // Raw returns a AnalyticsResult which can be used to access the raw byte data from search queries. // Calling this function invalidates the underlying AnalyticsResult which will no longer be able to be used. // VOLATILE: This API is subject to change at any time. func (r *AnalyticsResult) Raw() *AnalyticsResultRaw { vr := &AnalyticsResultRaw{ reader: r.reader, } r.reader = nil return vr } // Next assigns the next result from the results into the value pointer, returning whether the read was successful. func (r *AnalyticsResult) Next() bool { if r.reader == nil { return false } rowBytes := r.reader.NextRow() if rowBytes == nil { return false } r.rowBytes = rowBytes return true } // Row returns the value of the current row func (r *AnalyticsResult) Row(valuePtr interface{}) error { if r.reader == nil { return r.Err() } if r.rowBytes == nil { return ErrNoResult } if bytesPtr, ok := valuePtr.(*json.RawMessage); ok { *bytesPtr = r.rowBytes return nil } return json.Unmarshal(r.rowBytes, valuePtr) } // Err returns any errors that have occurred on the stream func (r *AnalyticsResult) Err() error { if r.reader == nil { return errors.New("result object is no longer valid") } err := r.reader.Err() if err != nil { return maybeEnhanceAnalyticsError(err) } return nil } // Close marks the results as closed, returning any errors that occurred during reading the results. func (r *AnalyticsResult) Close() error { if r.reader == nil { return r.Err() } err := r.reader.Close() if err != nil { return maybeEnhanceAnalyticsError(err) } return nil } // One assigns the first value from the results into the value pointer. // It will close the results but not before iterating through all remaining // results, as such this should only be used for very small resultsets - ideally // of, at most, length 1. func (r *AnalyticsResult) One(valuePtr interface{}) error { if r.reader == nil { return r.Err() } // Read the bytes from the first row valueBytes := r.reader.NextRow() if valueBytes == nil { return ErrNoResult } // Skip through the remaining rows for r.reader.NextRow() != nil { // do nothing with the row } return json.Unmarshal(valueBytes, valuePtr) } // MetaData returns any meta-data that was available from this query. Note that // the meta-data will only be available once the object has been closed (either // implicitly or explicitly). func (r *AnalyticsResult) MetaData() (*AnalyticsMetaData, error) { if r.reader == nil { return nil, r.Err() } metaDataBytes, err := r.reader.MetaData() if err != nil { return nil, err } var jsonResp jsonAnalyticsResponse err = json.Unmarshal(metaDataBytes, &jsonResp) if err != nil { return nil, err } var metaData AnalyticsMetaData err = metaData.fromData(jsonResp) if err != nil { return nil, err } return &metaData, nil } // AnalyticsQuery executes the analytics query statement on the server. func (c *Cluster) AnalyticsQuery(statement string, opts *AnalyticsOptions) (*AnalyticsResult, error) { if opts == nil { opts = &AnalyticsOptions{} } start := time.Now() defer c.meter.ValueRecord(meterValueServiceAnalytics, "analytics", start) span := createSpan(c.tracer, opts.ParentSpan, "analytics", "analytics") span.SetAttribute("db.statement", statement) defer span.End() timeout := opts.Timeout if opts.Timeout == 0 { timeout = c.timeoutsConfig.AnalyticsTimeout } deadline := time.Now().Add(timeout) retryStrategy := c.retryStrategyWrapper if opts.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(opts.RetryStrategy) } queryOpts, err := opts.toMap() if err != nil { return nil, AnalyticsError{ InnerError: wrapError(err, "failed to generate query options"), Statement: statement, ClientContextID: opts.ClientContextID, } } var priorityInt int32 if opts.Priority { priorityInt = -1 } queryOpts["statement"] = statement provider, err := c.getAnalyticsProvider() if err != nil { return nil, AnalyticsError{ InnerError: wrapError(err, "failed to get query provider"), Statement: statement, ClientContextID: maybeGetAnalyticsOption(queryOpts, "client_context_id"), } } return execAnalyticsQuery(opts.Context, span, queryOpts, priorityInt, deadline, retryStrategy, provider, c.tracer, opts.Internal.User) } func maybeGetAnalyticsOption(options map[string]interface{}, name string) string { if value, ok := options[name].(string); ok { return value } return "" } func execAnalyticsQuery( ctx context.Context, span RequestSpan, options map[string]interface{}, priority int32, deadline time.Time, retryStrategy *retryStrategyWrapper, provider analyticsProvider, tracer RequestTracer, user string, ) (*AnalyticsResult, error) { eSpan := createSpan(tracer, span, "request_encoding", "") reqBytes, err := json.Marshal(options) eSpan.End() if err != nil { return nil, AnalyticsError{ InnerError: wrapError(err, "failed to marshall query body"), Statement: maybeGetAnalyticsOption(options, "statement"), ClientContextID: maybeGetAnalyticsOption(options, "client_context_id"), } } res, err := provider.AnalyticsQuery(ctx, gocbcore.AnalyticsQueryOptions{ Payload: reqBytes, Priority: int(priority), RetryStrategy: retryStrategy, Deadline: deadline, TraceContext: span.Context(), User: user, }) if err != nil { return nil, maybeEnhanceAnalyticsError(err) } return newAnalyticsResult(res), nil } gocb-2.6.3/cluster_analyticsquery_test.go000066400000000000000000000475051441755043100206570ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/couchbase/gocbcore/v10" "github.com/stretchr/testify/mock" ) type testAnalyticsDataset struct { Results []testBreweryDocument jsonAnalyticsResponse } type analyticsIface interface { AnalyticsQuery(string, *AnalyticsOptions) (*AnalyticsResult, error) } func (suite *IntegrationTestSuite) TestClusterAnalyticsQuery() { suite.skipIfUnsupported(AnalyticsFeature) n := suite.setupClusterAnalytics() query := fmt.Sprintf("SELECT `testAnalytics`.* FROM `testAnalytics` WHERE service=? LIMIT %d;", n) suite.runAnalyticsTest(n, query, "", "", globalCluster) } func (suite *IntegrationTestSuite) runAnalyticsTest(n int, query, bucket, scope string, provider analyticsIface) { deadline := time.Now().Add(60 * time.Second) for { globalTracer.Reset() globalMeter.Reset() contextID := "contextID" result, err := provider.AnalyticsQuery(query, &AnalyticsOptions{ PositionalParameters: []interface{}{"analytics"}, ClientContextID: contextID, }) suite.Require().Nil(err, "Failed to execute query %v", err) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(1, len(nilParents)) suite.AssertHTTPOpSpan(nilParents[0], "analytics", HTTPOpSpanExpectations{ bucket: bucket, scope: scope, statement: query, numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "analytics", dispatchOperationID: "contextID", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "analytics", "analytics"), 1, false) var samples []interface{} for result.Next() { var sample interface{} err := result.Row(&sample) suite.Require().Nil(err, "Failed to get value from row %v", err) samples = append(samples, sample) } err = result.Err() suite.Require().Nil(err, "Result had error %v", err) metadata, err := result.MetaData() suite.Require().Nil(err, "Metadata had error: %v", err) suite.Assert().NotEmpty(metadata.RequestID) if n == len(samples) { return } sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for indexing") return } } } func (suite *IntegrationTestSuite) setupClusterAnalytics() int { n, err := suite.createBreweryDataset("beer_sample_brewery_five", "analytics", "", "") suite.Require().Nil(err, "Failed to create dataset %v", err) mgr := globalCluster.AnalyticsIndexes() err = mgr.CreateDataset("testAnalytics", globalBucket.Name(), &CreateAnalyticsDatasetOptions{ IgnoreIfExists: true, Timeout: 5 * time.Second, }) suite.Require().Nil(err, "Failed to create dataset %v", err) err = mgr.ConnectLink(&ConnectAnalyticsLinkOptions{ Timeout: 10 * time.Second, }) suite.Require().Nil(err, "Failed to connect link %v", err) return n } // We have to manually mock this because testify won't let return something which can iterate. type mockAnalyticsRowReader struct { Dataset []testBreweryDocument Meta []byte MetaErr error CloseErr error RowsErr error Suite *UnitTestSuite idx int } func (arr *mockAnalyticsRowReader) NextRow() []byte { if arr.idx == len(arr.Dataset) { return nil } idx := arr.idx arr.idx++ return arr.Suite.mustConvertToBytes(arr.Dataset[idx]) } func (arr *mockAnalyticsRowReader) MetaData() ([]byte, error) { return arr.Meta, arr.MetaErr } func (arr *mockAnalyticsRowReader) Close() error { return arr.CloseErr } func (arr *mockAnalyticsRowReader) Err() error { return arr.RowsErr } func (suite *IntegrationTestSuite) TestClusterAnalyticsQueryContext() { suite.skipIfUnsupported(AnalyticsFeature) ctx, cancel := context.WithCancel(context.Background()) cancel() res, err := globalCluster.AnalyticsQuery("SELECT 1=1", &AnalyticsOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) defer cancel() res, err = globalCluster.AnalyticsQuery("SELECT 1=1", &AnalyticsOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) } func (suite *UnitTestSuite) TestAnalyticsQuery() { var dataset testAnalyticsDataset err := loadJSONTestDataset("beer_sample_analytics_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockAnalyticsRowReader{ Dataset: dataset.Results, Meta: suite.mustConvertToBytes(dataset.jsonAnalyticsResponse), Suite: suite, } statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) suite.Assert().Equal(0, opts.Priority) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Contains(actualOptions, "statement") suite.Assert().Contains(actualOptions, "client_context_id") suite.Assert().Equal(statement, actualOptions["statement"]) }, reader) result, err := cluster.AnalyticsQuery(statement, nil) suite.Require().Nil(err, err) suite.Require().NotNil(result) var breweries []testBreweryDocument for result.Next() { var doc testBreweryDocument err := result.Row(&doc) suite.Require().Nil(err, err) breweries = append(breweries, doc) } suite.Assert().Len(breweries, len(dataset.Results)) err = result.Err() suite.Require().Nil(err, err) metadata, err := result.MetaData() suite.Require().Nil(err, err) var aMeta AnalyticsMetaData err = aMeta.fromData(dataset.jsonAnalyticsResponse) suite.Require().Nil(err, err) suite.Assert().Equal(&aMeta, metadata) } func (suite *UnitTestSuite) TestAnalyticsQueryResultsOne() { var dataset testAnalyticsDataset err := loadJSONTestDataset("beer_sample_analytics_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockAnalyticsRowReader{ Dataset: dataset.Results, Meta: suite.mustConvertToBytes(dataset.jsonAnalyticsResponse), Suite: suite, } result := &AnalyticsResult{ reader: reader, } var doc testBreweryDocument err = result.One(&doc) suite.Require().Nil(err, err) suite.Assert().Equal(dataset.Results[0], doc) // Test that One iterated all rows. var count int for result.Next() { count++ } suite.Assert().Zero(count) err = result.Err() suite.Require().Nil(err, err) metadata, err := result.MetaData() suite.Require().Nil(err, err) var aMeta AnalyticsMetaData err = aMeta.fromData(dataset.jsonAnalyticsResponse) suite.Require().Nil(err, err) suite.Assert().Equal(&aMeta, metadata) } func (suite *UnitTestSuite) TestAnalyticsQueryResultsErr() { reader := &mockAnalyticsRowReader{ RowsErr: errors.New("some error"), Suite: suite, } result := &AnalyticsResult{ reader: reader, } err := result.Err() suite.Require().NotNil(err, err) } func (suite *UnitTestSuite) TestAnalyticsQueryResultsCloseErr() { reader := &mockAnalyticsRowReader{ CloseErr: errors.New("some error"), Suite: suite, } result := &AnalyticsResult{ reader: reader, } err := result.Close() suite.Require().NotNil(err, err) } func (suite *UnitTestSuite) TestAnalyticsQueryUntypedError() { retErr := errors.New("an error") analyticsProvider := new(mockAnalyticsProvider) analyticsProvider. On("AnalyticsQuery", nil, mock.AnythingOfType("gocbcore.AnalyticsQueryOptions")). Return(nil, retErr) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.AnalyticsQuery("SELECT * FROM dataset", nil) suite.Require().Equal(retErr, err) suite.Require().Nil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryGocbcoreError() { retErr := &gocbcore.AnalyticsError{ Endpoint: "http://localhost:8095", Statement: "SELECT * FROM dataset", ClientContextID: "context", Errors: []gocbcore.AnalyticsErrorDesc{{Code: 24001, Message: "Compilation error:"}}, } analyticsProvider := new(mockAnalyticsProvider) analyticsProvider. On("AnalyticsQuery", nil, mock.AnythingOfType("gocbcore.AnalyticsQueryOptions")). Return(nil, retErr) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.AnalyticsQuery("SELECT * FROM dataset", nil) suite.Require().IsType(&AnalyticsError{}, err) suite.Require().Equal(&AnalyticsError{ Endpoint: "http://localhost:8095", Statement: "SELECT * FROM dataset", ClientContextID: "context", Errors: []AnalyticsErrorDesc{{Code: 24001, Message: "Compilation error:"}}, }, err) suite.Require().Nil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryPriority() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" analyticsProvider := new(mockAnalyticsProvider) analyticsProvider. On("AnalyticsQuery", nil, mock.AnythingOfType("gocbcore.AnalyticsQueryOptions")). Run(func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) suite.Assert().Equal(-1, opts.Priority) }). Return(reader, nil) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ Priority: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryTimeoutOption() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) suite.Assert().Equal(0, opts.Priority) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(20*time.Second)) || opts.Deadline.After(now.Add(25*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ Timeout: 25 * time.Second, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryGCCCPUnsupported() { retErr := errors.New("an error") analyticsProvider := new(mockAnalyticsProvider) analyticsProvider. On("AnalyticsQuery", nil, mock.AnythingOfType("gocbcore.AnalyticsQueryOptions")). Return(nil, retErr) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) _, err := cluster.AnalyticsQuery("SELECT * FROM dataset", nil) suite.Require().NotNil(err) } func (suite *UnitTestSuite) TestAnalyticsQueryNamedParams() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" params := map[string]interface{}{ "num": 1, "imafish": "namedbarry", "$cilit": "bang", } cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().NotEmpty(actualOptions["client_context_id"]) suite.Assert().Equal(float64(1), actualOptions["$num"]) suite.Assert().Equal("namedbarry", actualOptions["$imafish"]) suite.Assert().Equal("bang", actualOptions["$cilit"]) }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ NamedParameters: params, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryPositionalParams() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" params := []interface{}{float64(1), "imafish"} cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().NotEmpty(actualOptions["client_context_id"]) if suite.Assert().Contains(actualOptions, "args") { suite.Require().Equal(params, actualOptions["args"]) } }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ PositionalParameters: params, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryBothParams() { statement := "SELECT * FROM dataset" params := []interface{}{float64(1), "imafish"} namedParams := map[string]interface{}{ "num": 1, "imafish": "namedbarry", "$cilit": "bang", } analyticsProvider := new(mockAnalyticsProvider) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ PositionalParameters: params, NamedParameters: namedParams, }) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected invalid argument error was %s", err) } suite.Require().Nil(result) analyticsProvider.AssertNotCalled(suite.T(), "AnalyticsQuery") } func (suite *UnitTestSuite) TestAnalyticsQueryClientContextID() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" contextID := "62d29101-0c9f-400d-af2b-9bd44a557a7c" cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().Equal(contextID, actualOptions["client_context_id"]) }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ ClientContextID: contextID, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryRawParam() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" params := map[string]interface{}{ "raw": "param", } cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().NotEmpty(actualOptions["client_context_id"]) if suite.Assert().Contains(actualOptions, "raw") { suite.Require().Equal("param", actualOptions["raw"]) } }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ Raw: params, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryReadonly() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().NotEmpty(actualOptions["client_context_id"]) suite.Assert().Equal(true, actualOptions["readonly"]) }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ Readonly: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryConsistencyNotBounded() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().NotEmpty(actualOptions["client_context_id"]) suite.Assert().Equal("not_bounded", actualOptions["scan_consistency"]) }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ ScanConsistency: AnalyticsScanConsistencyNotBounded, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryConsistencyRequestPlus() { reader := new(mockAnalyticsRowReader) statement := "SELECT * FROM dataset" cluster := suite.analyticsCluster(nil, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.AnalyticsQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(statement, actualOptions["statement"]) suite.Assert().NotEmpty(actualOptions["client_context_id"]) suite.Assert().Equal("request_plus", actualOptions["scan_consistency"]) }, reader) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ ScanConsistency: AnalyticsScanConsistencyRequestPlus, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestAnalyticsQueryConsistencyInvalid() { statement := "SELECT * FROM dataset" analyticsProvider := new(mockAnalyticsProvider) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.AnalyticsQuery(statement, &AnalyticsOptions{ ScanConsistency: 5, }) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected invalid argument error was %s", err) } suite.Require().Nil(result) analyticsProvider.AssertNotCalled(suite.T(), "AnalyticsQuery") } func (suite *UnitTestSuite) analyticsCluster(ctx context.Context, runFn func(args mock.Arguments), reader analyticsRowReader) *Cluster { analyticsProvider := new(mockAnalyticsProvider) analyticsProvider. On("AnalyticsQuery", ctx, mock.AnythingOfType("gocbcore.AnalyticsQueryOptions")). Run(runFn). Return(reader, nil) cli := new(mockConnectionManager) cli.On("getAnalyticsProvider").Return(analyticsProvider, nil) cluster := suite.newCluster(cli) return cluster } func (suite *UnitTestSuite) TestAnalyticsQueryRaw() { var dataset testAnalyticsDataset err := loadJSONTestDataset("beer_sample_analytics_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockAnalyticsRowReader{ Dataset: dataset.Results, Meta: suite.mustConvertToBytes(dataset.jsonAnalyticsResponse), Suite: suite, } statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.analyticsCluster(nil, func(args mock.Arguments) {}, reader) result, err := cluster.AnalyticsQuery(statement, nil) suite.Require().Nil(err, err) suite.Require().NotNil(result) raw := result.Raw() suite.Assert().False(result.Next()) suite.Assert().Error(result.One([]string{})) suite.Assert().Error(result.Err()) suite.Assert().Error(result.Close()) suite.Assert().Error(result.Row([]string{})) _, err = result.MetaData() suite.Assert().Error(err) var i int for b := raw.NextBytes(); b != nil; b = raw.NextBytes() { suite.Assert().Equal(suite.mustConvertToBytes(dataset.Results[i]), b) i++ } err = raw.Err() suite.Require().Nil(err, err) metadata, err := raw.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(reader.Meta, metadata) } gocb-2.6.3/cluster_bucketmgr.go000066400000000000000000000544451441755043100165270ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "net/url" "strings" "time" "github.com/google/uuid" ) // BucketType specifies the kind of bucket. type BucketType string const ( // CouchbaseBucketType indicates a Couchbase bucket type. CouchbaseBucketType BucketType = "membase" // MemcachedBucketType indicates a Memcached bucket type. MemcachedBucketType BucketType = "memcached" // EphemeralBucketType indicates an Ephemeral bucket type. EphemeralBucketType BucketType = "ephemeral" ) // ConflictResolutionType specifies the kind of conflict resolution to use for a bucket. type ConflictResolutionType string const ( // ConflictResolutionTypeTimestamp specifies to use timestamp conflict resolution on the bucket. ConflictResolutionTypeTimestamp ConflictResolutionType = "lww" // ConflictResolutionTypeSequenceNumber specifies to use sequence number conflict resolution on the bucket. ConflictResolutionTypeSequenceNumber ConflictResolutionType = "seqno" // ConflictResolutionTypeCustom specifies to use a custom bucket conflict resolution. // In Couchbase Server 7.1, this feature is only available in "developer-preview" mode. See the UI XDCR settings // for the custom conflict resolution properties. // VOLATILE: This API is subject to change at any time. ConflictResolutionTypeCustom ConflictResolutionType = "custom" ) // EvictionPolicyType specifies the kind of eviction policy to use for a bucket. type EvictionPolicyType string const ( // EvictionPolicyTypeFull specifies to use full eviction for a couchbase bucket. EvictionPolicyTypeFull EvictionPolicyType = "fullEviction" // EvictionPolicyTypeValueOnly specifies to use value only eviction for a couchbase bucket. EvictionPolicyTypeValueOnly EvictionPolicyType = "valueOnly" // EvictionPolicyTypeNotRecentlyUsed specifies to use not recently used (nru) eviction for an ephemeral bucket. EvictionPolicyTypeNotRecentlyUsed EvictionPolicyType = "nruEviction" // EvictionPolicyTypeNRU specifies to use no eviction for an ephemeral bucket. EvictionPolicyTypeNoEviction EvictionPolicyType = "noEviction" ) // CompressionMode specifies the kind of compression to use for a bucket. type CompressionMode string const ( // CompressionModeOff specifies to use no compression for a bucket. CompressionModeOff CompressionMode = "off" // CompressionModePassive specifies to use passive compression for a bucket. CompressionModePassive CompressionMode = "passive" // CompressionModeActive specifies to use active compression for a bucket. CompressionModeActive CompressionMode = "active" ) // StorageBackend specifies the storage type to use for the bucket. type StorageBackend string const ( // StorageBackendCouchstore specifies to use the couchstore storage type. StorageBackendCouchstore StorageBackend = "couchstore" // StorageBackendMagma specifies to use the magma storage type. EE only. StorageBackendMagma StorageBackend = "magma" ) type jsonBucketSettings struct { Name string `json:"name"` Controllers struct { Flush string `json:"flush"` } `json:"controllers"` ReplicaIndex bool `json:"replicaIndex"` Quota struct { RAM uint64 `json:"ram"` RawRAM uint64 `json:"rawRAM"` } `json:"quota"` ReplicaNumber uint32 `json:"replicaNumber"` BucketType string `json:"bucketType"` ConflictResolutionType string `json:"conflictResolutionType"` EvictionPolicy string `json:"evictionPolicy"` MaxTTL uint32 `json:"maxTTL"` CompressionMode string `json:"compressionMode"` MinimumDurabilityLevel string `json:"durabilityMinLevel"` StorageBackend string `json:"storageBackend"` } // BucketSettings holds information about the settings for a bucket. type BucketSettings struct { Name string FlushEnabled bool ReplicaIndexDisabled bool // inverted so that zero value matches server default. RAMQuotaMB uint64 NumReplicas uint32 // NOTE: If not set this will set 0 replicas. BucketType BucketType // Defaults to CouchbaseBucketType. EvictionPolicy EvictionPolicyType // Deprecated: Use MaxExpiry instead. MaxTTL time.Duration MaxExpiry time.Duration CompressionMode CompressionMode MinimumDurabilityLevel DurabilityLevel StorageBackend StorageBackend } func (bs *BucketSettings) fromData(data jsonBucketSettings) error { bs.Name = data.Name bs.FlushEnabled = data.Controllers.Flush != "" bs.ReplicaIndexDisabled = !data.ReplicaIndex bs.RAMQuotaMB = data.Quota.RawRAM / 1024 / 1024 bs.NumReplicas = data.ReplicaNumber bs.EvictionPolicy = EvictionPolicyType(data.EvictionPolicy) bs.MaxTTL = time.Duration(data.MaxTTL) * time.Second bs.MaxExpiry = time.Duration(data.MaxTTL) * time.Second bs.CompressionMode = CompressionMode(data.CompressionMode) bs.MinimumDurabilityLevel = durabilityLevelFromManagementAPI(data.MinimumDurabilityLevel) bs.StorageBackend = StorageBackend(data.StorageBackend) switch data.BucketType { case "membase": bs.BucketType = CouchbaseBucketType case "memcached": bs.BucketType = MemcachedBucketType case "ephemeral": bs.BucketType = EphemeralBucketType default: return errors.New("unrecognized bucket type string") } return nil } type bucketMgrErrorResp struct { Errors map[string]string `json:"errors"` } func (bm *BucketManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read bucket manager response body: %s", err) return nil } if resp.StatusCode == 404 { // If it was a 404 then there's no chance of the response body containing any structure if strings.Contains(strings.ToLower(string(b)), "resource not found") { return makeGenericMgmtError(ErrBucketNotFound, req, resp, string(b)) } return makeGenericMgmtError(errors.New(string(b)), req, resp, string(b)) } if err := checkForRateLimitError(resp.StatusCode, string(b)); err != nil { return makeGenericMgmtError(err, req, resp, string(b)) } var mgrErr bucketMgrErrorResp err = json.Unmarshal(b, &mgrErr) if err != nil { logDebugf("Failed to unmarshal error body: %s", err) return makeGenericMgmtError(errors.New(string(b)), req, resp, string(b)) } var bodyErr error var firstErr string for _, err := range mgrErr.Errors { firstErr = strings.ToLower(err) break } if strings.Contains(firstErr, "bucket with given name already exists") { bodyErr = ErrBucketExists } else { bodyErr = errors.New(firstErr) } return makeGenericMgmtError(bodyErr, req, resp, string(b)) } // Flush doesn't use the same body format as anything else... func (bm *BucketManager) tryParseFlushErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read bucket manager response body: %s", err) return makeMgmtBadStatusError("failed to flush bucket", req, resp) } if resp.StatusCode == 404 { // If it was a 404 then there's no chance of the response body containing any structure if strings.Contains(strings.ToLower(string(b)), "resource not found") { return makeGenericMgmtError(ErrBucketNotFound, req, resp, string(b)) } return makeGenericMgmtError(errors.New(string(b)), req, resp, string(b)) } var bodyErrMsgs map[string]string err = json.Unmarshal(b, &bodyErrMsgs) if err != nil { return errors.New(string(b)) } if errMsg, ok := bodyErrMsgs["_"]; ok { if strings.Contains(strings.ToLower(errMsg), "flush is disabled") { return makeGenericMgmtError(ErrBucketNotFlushable, req, resp, string(b)) } } return errors.New(string(b)) } // BucketManager provides methods for performing bucket management operations. // See BucketManager for methods that allow creating and removing buckets themselves. type BucketManager struct { provider mgmtProvider tracer RequestTracer meter *meterWrapper } // GetBucketOptions is the set of options available to the bucket manager GetBucket operation. type GetBucketOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetBucket returns settings for a bucket on the cluster. func (bm *BucketManager) GetBucket(bucketName string, opts *GetBucketOptions) (*BucketSettings, error) { if opts == nil { opts = &GetBucketOptions{} } start := time.Now() defer bm.meter.ValueRecord(meterValueServiceManagement, "manager_bucket_get_bucket", start) path := fmt.Sprintf("/pools/default/buckets/%s", bucketName) span := createSpan(bm.tracer, opts.ParentSpan, "manager_bucket_create_bucket", "management") span.SetAttribute("db.name", bucketName) span.SetAttribute("db.operation", "GET "+path) defer span.End() return bm.get(opts.Context, span.Context(), path, opts.RetryStrategy, opts.Timeout) } func (bm *BucketManager) get(ctx context.Context, tracectx RequestSpanContext, path string, strategy RetryStrategy, timeout time.Duration) (*BucketSettings, error) { req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "GET", IsIdempotent: true, RetryStrategy: strategy, UniqueID: uuid.New().String(), Timeout: timeout, parentSpanCtx: tracectx, } resp, err := bm.provider.executeMgmtRequest(ctx, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { bktErr := bm.tryParseErrorMessage(&req, resp) if bktErr != nil { return nil, bktErr } return nil, makeMgmtBadStatusError("failed to get bucket", &req, resp) } var bucketData jsonBucketSettings jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&bucketData) if err != nil { return nil, err } var settings BucketSettings err = settings.fromData(bucketData) if err != nil { return nil, err } return &settings, nil } // GetAllBucketsOptions is the set of options available to the bucket manager GetAll operation. type GetAllBucketsOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllBuckets returns a list of all active buckets on the cluster. func (bm *BucketManager) GetAllBuckets(opts *GetAllBucketsOptions) (map[string]BucketSettings, error) { if opts == nil { opts = &GetAllBucketsOptions{} } start := time.Now() defer bm.meter.ValueRecord(meterValueServiceManagement, "manager_bucket_get_all_buckets", start) span := createSpan(bm.tracer, opts.ParentSpan, "manager_bucket_get_all_buckets", "management") span.SetAttribute("db.operation", "GET /pools/default/buckets") defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: "/pools/default/buckets", Method: "GET", IsIdempotent: true, RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := bm.provider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { bktErr := bm.tryParseErrorMessage(&req, resp) if bktErr != nil { return nil, bktErr } return nil, makeMgmtBadStatusError("failed to get all buckets", &req, resp) } var bucketsData []*jsonBucketSettings jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&bucketsData) if err != nil { return nil, err } buckets := make(map[string]BucketSettings, len(bucketsData)) for _, bucketData := range bucketsData { var bucket BucketSettings err := bucket.fromData(*bucketData) if err != nil { return nil, err } buckets[bucket.Name] = bucket } return buckets, nil } // CreateBucketSettings are the settings available when creating a bucket. type CreateBucketSettings struct { BucketSettings ConflictResolutionType ConflictResolutionType } // CreateBucketOptions is the set of options available to the bucket manager CreateBucket operation. type CreateBucketOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateBucket creates a bucket on the cluster. func (bm *BucketManager) CreateBucket(settings CreateBucketSettings, opts *CreateBucketOptions) error { if opts == nil { opts = &CreateBucketOptions{} } start := time.Now() defer bm.meter.ValueRecord(meterValueServiceManagement, "manager_bucket_create_bucket", start) span := createSpan(bm.tracer, opts.ParentSpan, "manager_bucket_create_bucket", "management") span.SetAttribute("db.name", settings.Name) span.SetAttribute("db.operation", "POST /pools/default/buckets") defer span.End() posts, err := bm.settingsToPostData(&settings.BucketSettings) if err != nil { return err } if settings.ConflictResolutionType != "" { posts.Add("conflictResolutionType", string(settings.ConflictResolutionType)) } eSpan := createSpan(bm.tracer, span, "request_encoding", "") d := posts.Encode() eSpan.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: "/pools/default/buckets", Method: "POST", Body: []byte(d), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := bm.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 202 { bktErr := bm.tryParseErrorMessage(&req, resp) if bktErr != nil { return bktErr } return makeMgmtBadStatusError("failed to create bucket", &req, resp) } return nil } // UpdateBucketOptions is the set of options available to the bucket manager UpdateBucket operation. type UpdateBucketOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UpdateBucket updates a bucket on the cluster. func (bm *BucketManager) UpdateBucket(settings BucketSettings, opts *UpdateBucketOptions) error { if opts == nil { opts = &UpdateBucketOptions{} } start := time.Now() defer bm.meter.ValueRecord(meterValueServiceManagement, "manager_bucket_update_bucket", start) path := fmt.Sprintf("/pools/default/buckets/%s", settings.Name) span := createSpan(bm.tracer, opts.ParentSpan, "manager_bucket_update_bucket", "management") span.SetAttribute("db.name", settings.Name) span.SetAttribute("db.operation", "POST "+path) defer span.End() posts, err := bm.settingsToPostData(&settings) if err != nil { return err } eSpan := createSpan(bm.tracer, span, "request_encoding", "") d := posts.Encode() eSpan.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "POST", Body: []byte(d), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := bm.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { bktErr := bm.tryParseErrorMessage(&req, resp) if bktErr != nil { return bktErr } return makeMgmtBadStatusError("failed to update bucket", &req, resp) } return nil } // DropBucketOptions is the set of options available to the bucket manager DropBucket operation. type DropBucketOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropBucket will delete a bucket from the cluster by name. func (bm *BucketManager) DropBucket(name string, opts *DropBucketOptions) error { if opts == nil { opts = &DropBucketOptions{} } start := time.Now() defer bm.meter.ValueRecord(meterValueServiceManagement, "manager_bucket_drop_bucket", start) path := fmt.Sprintf("/pools/default/buckets/%s", name) span := createSpan(bm.tracer, opts.ParentSpan, "manager_bucket_drop_bucket", "management") span.SetAttribute("db.name", name) span.SetAttribute("db.operation", "DELETE "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "DELETE", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := bm.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { bktErr := bm.tryParseErrorMessage(&req, resp) if bktErr != nil { return bktErr } return makeMgmtBadStatusError("failed to drop bucket", &req, resp) } return nil } // FlushBucketOptions is the set of options available to the bucket manager FlushBucket operation. type FlushBucketOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // FlushBucket will delete all the of the data from a bucket. // Keep in mind that you must have flushing enabled in the buckets configuration. func (bm *BucketManager) FlushBucket(name string, opts *FlushBucketOptions) error { if opts == nil { opts = &FlushBucketOptions{} } start := time.Now() defer bm.meter.ValueRecord(meterValueServiceManagement, "manager_bucket_flush_bucket", start) path := fmt.Sprintf("/pools/default/buckets/%s/controller/doFlush", name) span := createSpan(bm.tracer, opts.ParentSpan, "manager_bucket_flush_bucket", "management") span.SetAttribute("db.name", name) span.SetAttribute("db.operation", "POST "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "POST", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := bm.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { return bm.tryParseFlushErrorMessage(&req, resp) } return nil } func (bm *BucketManager) settingsToPostData(settings *BucketSettings) (url.Values, error) { posts := url.Values{} if settings.Name == "" { return nil, makeInvalidArgumentsError("Name invalid, must be set.") } if settings.RAMQuotaMB < 100 { return nil, makeInvalidArgumentsError("Memory quota invalid, must be greater than 100MB") } if (settings.MaxTTL > 0 || settings.MaxExpiry > 0) && settings.BucketType == MemcachedBucketType { return nil, makeInvalidArgumentsError("maxExpiry is not supported for memcached buckets") } posts.Add("name", settings.Name) // posts.Add("saslPassword", settings.Password) if settings.FlushEnabled { posts.Add("flushEnabled", "1") } else { posts.Add("flushEnabled", "0") } // replicaIndex can't be set at all on ephemeral buckets. if settings.BucketType != EphemeralBucketType { if settings.ReplicaIndexDisabled { posts.Add("replicaIndex", "0") } else { posts.Add("replicaIndex", "1") } } switch settings.BucketType { case CouchbaseBucketType: posts.Add("bucketType", string(settings.BucketType)) posts.Add("replicaNumber", fmt.Sprintf("%d", settings.NumReplicas)) case MemcachedBucketType: posts.Add("bucketType", string(settings.BucketType)) if settings.NumReplicas > 0 { return nil, makeInvalidArgumentsError("replicas cannot be used with memcached buckets") } case EphemeralBucketType: posts.Add("bucketType", string(settings.BucketType)) posts.Add("replicaNumber", fmt.Sprintf("%d", settings.NumReplicas)) default: return nil, makeInvalidArgumentsError("unrecognized bucket type") } posts.Add("ramQuotaMB", fmt.Sprintf("%d", settings.RAMQuotaMB)) if settings.EvictionPolicy != "" { switch settings.BucketType { case MemcachedBucketType: return nil, makeInvalidArgumentsError("eviction policy is not valid for memcached buckets") case CouchbaseBucketType: if settings.EvictionPolicy == EvictionPolicyTypeNoEviction || settings.EvictionPolicy == EvictionPolicyTypeNotRecentlyUsed { return nil, makeInvalidArgumentsError("eviction policy is not valid for couchbase buckets") } case EphemeralBucketType: if settings.EvictionPolicy == EvictionPolicyTypeFull || settings.EvictionPolicy == EvictionPolicyTypeValueOnly { return nil, makeInvalidArgumentsError("eviction policy is not valid for ephemeral buckets") } } posts.Add("evictionPolicy", string(settings.EvictionPolicy)) } if settings.MaxTTL > 0 { posts.Add("maxTTL", fmt.Sprintf("%d", settings.MaxTTL/time.Second)) } if settings.MaxExpiry > 0 { posts.Add("maxTTL", fmt.Sprintf("%d", settings.MaxExpiry/time.Second)) } if settings.CompressionMode != "" { posts.Add("compressionMode", string(settings.CompressionMode)) } if settings.MinimumDurabilityLevel > DurabilityLevelNone { level, err := settings.MinimumDurabilityLevel.toManagementAPI() if err != nil { return nil, err } posts.Add("durabilityMinLevel", level) } if settings.StorageBackend != "" { posts.Add("storageBackend", string(settings.StorageBackend)) } return posts, nil } gocb-2.6.3/cluster_bucketmgr_test.go000066400000000000000000000453251441755043100175630ustar00rootroot00000000000000package gocb import ( "errors" "testing" "time" ) func (suite *IntegrationTestSuite) TestBucketMgrOps() { suite.skipIfUnsupported(BucketMgrFeature) mgr := globalCluster.Buckets() settings := BucketSettings{ Name: "test22", RAMQuotaMB: 100, NumReplicas: 1, BucketType: CouchbaseBucketType, EvictionPolicy: EvictionPolicyTypeValueOnly, FlushEnabled: true, MaxExpiry: 10 * time.Second, CompressionMode: CompressionModeActive, ReplicaIndexDisabled: true, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, ConflictResolutionType: ConflictResolutionTypeSequenceNumber, }, nil) if err != nil { suite.T().Fatalf("Failed to create bucket %v", err) } // Buckets don't become available immediately so we need to do a bit of polling to see if it comes online. var bucket *BucketSettings success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { bucket, err = mgr.GetBucket("test22", nil) if err != nil { suite.T().Logf("Failed to get bucket %v", err) return false } return true }) suite.Assert().True(success, "GetBucket failed to execute within the required time") suite.Assert().Equal(settings.BucketType, bucket.BucketType) suite.Assert().Equal(settings.Name, bucket.Name) suite.Assert().Equal(settings.RAMQuotaMB, bucket.RAMQuotaMB) suite.Assert().Equal(settings.NumReplicas, bucket.NumReplicas) suite.Assert().Equal(settings.FlushEnabled, bucket.FlushEnabled) suite.Assert().Equal(settings.MaxExpiry, bucket.MaxExpiry) suite.Assert().Equal(settings.EvictionPolicy, bucket.EvictionPolicy) suite.Assert().Equal(settings.CompressionMode, bucket.CompressionMode) suite.Assert().True(bucket.ReplicaIndexDisabled) err = mgr.UpdateBucket(*bucket, nil) if err != nil { suite.T().Fatalf("Failed to upsert bucket after get %v", err) } buckets, err := mgr.GetAllBuckets(nil) if err != nil { suite.T().Fatalf("Failed to get all buckets %v", err) } if len(buckets) == 0 { suite.T().Fatalf("Bucket settings list was length 0") } var b *BucketSettings for _, bucket := range buckets { if bucket.Name == "test22" { b = &bucket } } if b == nil { suite.T().Fatalf("Test bucket was not found in list of bucket settings, %v", buckets) } success = suite.tryUntil(time.Now().Add(5*time.Second), 50*time.Millisecond, func() bool { err = mgr.FlushBucket("test22", nil) if err != nil { suite.T().Logf("Flush bucket failed with %s", err) return false } return true }) if !success { suite.T().Fatal("Wait time for bucket flush expired") } success = suite.tryUntil(time.Now().Add(5*time.Second), 50*time.Millisecond, func() bool { err = mgr.DropBucket("test22", nil) if err != nil { suite.T().Logf("Drop bucket failed with %s", err) return false } return true }) if !success { suite.T().Fatal("Wait time for drop bucket expired") } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_create_bucket"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_update_bucket"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_get_bucket"), 1, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_get_all_buckets"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_flush_bucket"), 1, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_drop_bucket"), 1, true) } func (suite *IntegrationTestSuite) TestBucketMgrFlushDisabled() { suite.skipIfUnsupported(BucketMgrFeature) mgr := globalCluster.Buckets() deadline := time.Now().Add(10 * time.Second) for { err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: BucketSettings{ Name: "testFlush", RAMQuotaMB: 100, NumReplicas: 0, BucketType: CouchbaseBucketType, EvictionPolicy: EvictionPolicyTypeValueOnly, FlushEnabled: false, MaxExpiry: 10, CompressionMode: CompressionModeActive, ReplicaIndexDisabled: true, }, ConflictResolutionType: ConflictResolutionTypeSequenceNumber, }, nil) if err == nil { break } suite.T().Logf("Failed to create bucket %v", err) if time.Now().After(deadline) { suite.T().Fatalf("Deadline exceeded for create bucket, failing test") } } defer mgr.DropBucket("testFlush", nil) suite.Assert().True(suite.tryUntil(time.Now().Add(10*time.Second), 500*time.Millisecond, func() bool { err := mgr.FlushBucket("testFlush", nil) if err == nil { suite.T().Fatalf("Expected to fail to flush bucket") } if errors.Is(err, ErrBucketNotFlushable) { return true } suite.T().Logf("Expected error to be bucket not flushable but was %v", err) return false })) } func (suite *IntegrationTestSuite) TestBucketMgrBucketNotExist() { suite.skipIfUnsupported(BucketMgrFeature) mgr := globalCluster.Buckets() _, err := mgr.GetBucket("testBucketThatDoesNotExist", nil) if err == nil { suite.T().Fatalf("Expected to fail to get bucket") } if !errors.Is(err, ErrBucketNotFound) { suite.T().Fatalf("Expected error to be bucket not found but was %v", err) } } func (suite *IntegrationTestSuite) TestBucketMgrMemcached() { suite.skipIfUnsupported(BucketMgrFeature) mgr := globalCluster.Buckets() settings := BucketSettings{ Name: "testmemd", RAMQuotaMB: 100, NumReplicas: 0, BucketType: MemcachedBucketType, FlushEnabled: true, ReplicaIndexDisabled: true, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if err != nil { suite.T().Fatalf("Failed to create bucket %v", err) } defer mgr.DropBucket("testmemd", nil) // Buckets don't become available immediately so we need to do a bit of polling to see if it comes online. var bucket *BucketSettings success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { bucket, err = mgr.GetBucket("testmemd", nil) if err != nil { suite.T().Logf("Failed to get bucket %v", err) return false } return true }) suite.Require().True(success, "GetBucket failed to execute within the required time") suite.Assert().Equal(settings.BucketType, bucket.BucketType) suite.Assert().Equal(settings.Name, bucket.Name) suite.Assert().Equal(settings.RAMQuotaMB, bucket.RAMQuotaMB) suite.Assert().Equal(settings.NumReplicas, bucket.NumReplicas) suite.Assert().Equal(settings.FlushEnabled, bucket.FlushEnabled) suite.Assert().Equal(time.Duration(0), bucket.MaxExpiry) suite.Assert().Equal(CompressionModeOff, bucket.CompressionMode) suite.Assert().True(bucket.ReplicaIndexDisabled) settings.MaxExpiry = 10 * time.Second err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected invalid argument error but was %v", err) } } func (suite *IntegrationTestSuite) TestBucketMgrEphemeral() { suite.skipIfUnsupported(BucketMgrFeature) mgr := globalCluster.Buckets() settings := BucketSettings{ Name: "testeph", RAMQuotaMB: 100, NumReplicas: 1, BucketType: EphemeralBucketType, FlushEnabled: true, MaxExpiry: 10 * time.Second, EvictionPolicy: EvictionPolicyTypeNoEviction, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if err != nil { suite.T().Fatalf("Failed to create bucket %v", err) } defer mgr.DropBucket("testeph", nil) // Buckets don't become available immediately so we need to do a bit of polling to see if it comes online. var bucket *BucketSettings success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { bucket, err = mgr.GetBucket("testeph", nil) if err != nil { suite.T().Logf("Failed to get bucket %v", err) return false } return true }) suite.Require().True(success, "GetBucket failed to execute within the required time") suite.Assert().Equal(settings.BucketType, bucket.BucketType) suite.Assert().Equal(settings.Name, bucket.Name) suite.Assert().Equal(settings.RAMQuotaMB, bucket.RAMQuotaMB) suite.Assert().Equal(settings.NumReplicas, bucket.NumReplicas) suite.Assert().Equal(settings.FlushEnabled, bucket.FlushEnabled) suite.Assert().Equal(settings.MaxExpiry, bucket.MaxExpiry) suite.Assert().Equal(CompressionModePassive, bucket.CompressionMode) suite.Assert().Equal(settings.EvictionPolicy, bucket.EvictionPolicy) suite.Assert().True(bucket.ReplicaIndexDisabled) settings = BucketSettings{ Name: "testephNRU", RAMQuotaMB: 100, NumReplicas: 1, BucketType: EphemeralBucketType, FlushEnabled: true, MaxExpiry: 10 * time.Second, EvictionPolicy: EvictionPolicyTypeNotRecentlyUsed, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if err != nil { suite.T().Fatalf("Failed to create bucket %v", err) } defer mgr.DropBucket("testephNRU", nil) // Buckets don't become available immediately so we need to do a bit of polling to see if it comes online. success = suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { bucket, err = mgr.GetBucket("testephNRU", nil) if err != nil { suite.T().Logf("Failed to get bucket %v", err) return false } return true }) suite.Require().True(success, "GetBucket failed to execute within the required time") suite.Assert().Equal(settings.BucketType, bucket.BucketType) suite.Assert().Equal(settings.Name, bucket.Name) suite.Assert().Equal(settings.RAMQuotaMB, bucket.RAMQuotaMB) suite.Assert().Equal(settings.NumReplicas, bucket.NumReplicas) suite.Assert().Equal(settings.FlushEnabled, bucket.FlushEnabled) suite.Assert().Equal(settings.MaxExpiry, bucket.MaxExpiry) suite.Assert().Equal(CompressionModePassive, bucket.CompressionMode) suite.Assert().Equal(settings.EvictionPolicy, bucket.EvictionPolicy) suite.Assert().True(bucket.ReplicaIndexDisabled) } func (suite *IntegrationTestSuite) TestBucketMgrInvalidEviction() { mgr := globalCluster.Buckets() settings := BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: MemcachedBucketType, EvictionPolicy: EvictionPolicyTypeValueOnly, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: MemcachedBucketType, EvictionPolicy: EvictionPolicyTypeFull, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: MemcachedBucketType, EvictionPolicy: EvictionPolicyTypeNotRecentlyUsed, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: MemcachedBucketType, EvictionPolicy: EvictionPolicyTypeNoEviction, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: CouchbaseBucketType, EvictionPolicy: EvictionPolicyTypeNoEviction, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: CouchbaseBucketType, EvictionPolicy: EvictionPolicyTypeNotRecentlyUsed, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: EphemeralBucketType, EvictionPolicy: EvictionPolicyTypeValueOnly, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } settings = BucketSettings{ Name: "test", RAMQuotaMB: 100, NumReplicas: 0, BucketType: EphemeralBucketType, EvictionPolicy: EvictionPolicyTypeFull, } err = mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Error should have been invalid argument, was %v", err) } } func (suite *IntegrationTestSuite) TestBucketMgrMinDurability() { suite.skipIfUnsupported(BucketMgrFeature) suite.skipIfUnsupported(BucketMgrDurabilityFeature) mgr := globalCluster.Buckets() type tCase struct { name string settings BucketSettings } testCases := []tCase{ { name: "none", settings: BucketSettings{ Name: "testnone", RAMQuotaMB: 100, BucketType: CouchbaseBucketType, MinimumDurabilityLevel: DurabilityLevelNone, }, }, { name: "majority", settings: BucketSettings{ Name: "testmajority", RAMQuotaMB: 100, BucketType: CouchbaseBucketType, MinimumDurabilityLevel: DurabilityLevelMajority, }, }, { name: "majorityPersistToMaster", settings: BucketSettings{ Name: "testmajorityPersistToMaster", RAMQuotaMB: 100, BucketType: CouchbaseBucketType, MinimumDurabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, }, }, { name: "persistToMajority", settings: BucketSettings{ Name: "testpersistToMajority", RAMQuotaMB: 100, BucketType: CouchbaseBucketType, MinimumDurabilityLevel: DurabilityLevelPersistToMajority, }, }, } for _, tCase := range testCases { suite.T().Run(tCase.name, func(te *testing.T) { err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: tCase.settings, ConflictResolutionType: ConflictResolutionTypeSequenceNumber, }, nil) if err != nil { te.Fatalf("Failed to create bucket %v", err) } defer mgr.DropBucket(tCase.settings.Name, nil) // Buckets don't become available immediately so we need to do a bit of polling to see if it comes online. var bucket *BucketSettings success := suite.tryUntil(time.Now().Add(5*time.Second), 250*time.Millisecond, func() bool { bucket, err = mgr.GetBucket(tCase.settings.Name, nil) if err != nil { te.Logf("Failed to get bucket %v", err) return false } return true }) if !success { te.Fatalf("GetBucket failed to execute within the required time") } if bucket.MinimumDurabilityLevel != tCase.settings.MinimumDurabilityLevel { te.Fatalf("Expected minimum durability level to be %d but was %d", tCase.settings.MinimumDurabilityLevel, bucket.MinimumDurabilityLevel) } }) } } func (suite *IntegrationTestSuite) TestBucketMgrFlushBucketNotFound() { suite.skipIfUnsupported(BucketMgrFeature) mgr := globalCluster.Buckets() err := mgr.FlushBucket("testFlushBucketNotFound", nil) if err == nil { suite.T().Fatalf("Expected to fail to flush bucket") } if !errors.Is(err, ErrBucketNotFound) { suite.T().Fatalf("Expected error to be bucket not found but was %v", err) } } func (suite *IntegrationTestSuite) TestBucketMgrStorageBackendCouchstore() { suite.skipIfUnsupported(BucketMgrFeature) suite.skipIfUnsupported(StorageBackendFeature) mgr := globalCluster.Buckets() bName := "testcouchbasestorage" settings := BucketSettings{ Name: bName, RAMQuotaMB: 100, NumReplicas: 1, BucketType: CouchbaseBucketType, StorageBackend: StorageBackendCouchstore, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) suite.Require().Nil(err, err) defer mgr.DropBucket(bName, nil) b, err := mgr.GetBucket(bName, nil) suite.Require().Nil(err, err) suite.Assert().Equal(StorageBackendCouchstore, b.StorageBackend) } func (suite *IntegrationTestSuite) TestBucketMgrStorageBackendMagma() { suite.skipIfUnsupported(BucketMgrFeature) suite.skipIfUnsupported(StorageBackendFeature) mgr := globalCluster.Buckets() bName := "magma" settings := BucketSettings{ Name: bName, RAMQuotaMB: 1024, NumReplicas: 1, BucketType: CouchbaseBucketType, StorageBackend: StorageBackendMagma, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, }, nil) suite.Require().Nil(err, err) defer mgr.DropBucket(bName, nil) var bucket *BucketSettings success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { bucket, err = mgr.GetBucket(bName, nil) if err != nil { suite.T().Logf("Failed to get bucket %v", err) return false } return true }) suite.Assert().True(success, "GetBucket failed to execute within the required time") suite.Assert().Equal(StorageBackendMagma, bucket.StorageBackend) suite.Assert().Equal(1024, int(bucket.RAMQuotaMB)) bucket.RAMQuotaMB = 1124 err = mgr.UpdateBucket(*bucket, nil) suite.Require().Nil(err, err) bucket, err = mgr.GetBucket(bName, nil) suite.Require().Nil(err, err) suite.Assert().Equal(StorageBackendMagma, bucket.StorageBackend) suite.Assert().Equal(1124, int(bucket.RAMQuotaMB)) } func (suite *IntegrationTestSuite) TestBucketMgrCustomConflictResolution() { suite.skipIfUnsupported(BucketMgrFeature) suite.skipIfUnsupported(CustomConflictResolutionFeature) mgr := globalCluster.Buckets() bName := "testcouchbaseccr" settings := BucketSettings{ Name: bName, RAMQuotaMB: 100, NumReplicas: 1, BucketType: CouchbaseBucketType, StorageBackend: StorageBackendCouchstore, } err := mgr.CreateBucket(CreateBucketSettings{ BucketSettings: settings, ConflictResolutionType: ConflictResolutionTypeCustom, }, nil) suite.Require().Nil(err, err) defer mgr.DropBucket(bName, nil) //Can't check Conflict resolution of bucket b, err := mgr.GetBucket(bName, nil) suite.Require().Nil(err, err) suite.Assert().Equal(bName, b.Name) } gocb-2.6.3/cluster_diag.go000066400000000000000000000067601441755043100154450ustar00rootroot00000000000000package gocb import ( "encoding/json" "time" "github.com/couchbase/gocbcore/v10" "github.com/google/uuid" ) // EndPointDiagnostics represents a single entry in a diagnostics report. type EndPointDiagnostics struct { Type ServiceType ID string Local string Remote string LastActivity time.Time State EndpointState Namespace string } // DiagnosticsResult encapsulates the results of a Diagnostics operation. type DiagnosticsResult struct { ID string Services map[string][]EndPointDiagnostics sdk string State ClusterState } type jsonDiagnosticEntry struct { ID string `json:"id,omitempty"` LastActivityUs uint64 `json:"last_activity_us,omitempty"` Remote string `json:"remote,omitempty"` Local string `json:"local,omitempty"` State string `json:"state,omitempty"` Details string `json:"details,omitempty"` Namespace string `json:"namespace,omitempty"` } type jsonDiagnosticReport struct { Version int16 `json:"version"` SDK string `json:"sdk,omitempty"` ID string `json:"id,omitempty"` Services map[string][]jsonDiagnosticEntry `json:"services"` State string `json:"state"` } // MarshalJSON generates a JSON representation of this diagnostics report. func (report *DiagnosticsResult) MarshalJSON() ([]byte, error) { jsonReport := jsonDiagnosticReport{ Version: 2, SDK: report.sdk, ID: report.ID, Services: make(map[string][]jsonDiagnosticEntry), State: clusterStateToString(report.State), } for _, serviceType := range report.Services { for _, service := range serviceType { serviceStr := serviceTypeToString(service.Type) stateStr := endpointStateToString(service.State) jsonReport.Services[serviceStr] = append(jsonReport.Services[serviceStr], jsonDiagnosticEntry{ ID: service.ID, LastActivityUs: uint64(time.Since(service.LastActivity).Nanoseconds()), Remote: service.Remote, Local: service.Local, State: stateStr, Details: "", Namespace: service.Namespace, }) } } return json.Marshal(&jsonReport) } // DiagnosticsOptions are the options that are available for use with the Diagnostics operation. type DiagnosticsOptions struct { ReportID string } // Diagnostics returns information about the internal state of the SDK. func (c *Cluster) Diagnostics(opts *DiagnosticsOptions) (*DiagnosticsResult, error) { if opts == nil { opts = &DiagnosticsOptions{} } if opts.ReportID == "" { opts.ReportID = uuid.New().String() } provider, err := c.getDiagnosticsProvider() if err != nil { return nil, err } agentReport, err := provider.Diagnostics(gocbcore.DiagnosticsOptions{}) if err != nil { return nil, err } report := &DiagnosticsResult{ ID: opts.ReportID, Services: make(map[string][]EndPointDiagnostics), sdk: Identifier(), State: ClusterState(agentReport.State), } report.Services["kv"] = make([]EndPointDiagnostics, 0) for _, conn := range agentReport.MemdConns { state := EndpointState(conn.State) report.Services["kv"] = append(report.Services["kv"], EndPointDiagnostics{ Type: ServiceTypeKeyValue, State: state, Local: conn.LocalAddr, Remote: conn.RemoteAddr, LastActivity: conn.LastActivity, Namespace: conn.Scope, ID: conn.ID, }) } return report, nil } gocb-2.6.3/cluster_diag_test.go000066400000000000000000000130061441755043100164730ustar00rootroot00000000000000package gocb import ( "encoding/json" "time" "github.com/stretchr/testify/mock" "github.com/couchbase/gocbcore/v10" ) func (suite *UnitTestSuite) TestDiagnostics() { layout := "2006-01-02T15:04:05.000Z" date1, err := time.Parse(layout, "2014-11-12T11:45:26.371Z") if err != nil { suite.T().Fatalf("Failed to parse date: %v", err) } date2, err := time.Parse(layout, "2017-11-12T11:45:26.371Z") if err != nil { suite.T().Fatalf("Failed to parse date: %v", err) } info := &gocbcore.DiagnosticInfo{ ConfigRev: 1, MemdConns: []gocbcore.MemdConnInfo{ { LastActivity: date1, LocalAddr: "10.112.191.101", RemoteAddr: "10.112.191.102", Scope: "bucket", State: gocbcore.EndpointStateConnected, ID: "0xc000094120", }, { LastActivity: date2, LocalAddr: "", RemoteAddr: "", ID: "", State: gocbcore.EndpointStateDisconnected, }, }, } provider := new(mockDiagnosticsProvider) provider. On("Diagnostics", mock.AnythingOfType("gocbcore.DiagnosticsOptions")). Return(info, nil) cli := new(mockConnectionManager) cli.On("getDiagnosticsProvider", "").Return(provider, nil) c := &Cluster{ connectionManager: cli, } report, err := c.Diagnostics(nil) if err != nil { suite.T().Fatalf("Expected error to be nil but was %v", err) } if report.ID == "" { suite.T().Fatalf("Report ID should have been not empty") } services, ok := report.Services["kv"] if !ok { suite.T().Fatalf("Report missing kv service") } if len(services) != len(info.MemdConns) { suite.T().Fatalf("Expected Services length to be %d but was %d", len(info.MemdConns), len(report.Services)) } for i, service := range services { if service.Type != ServiceTypeKeyValue { suite.T().Fatalf("Expected service to be KeyValueService but was %d", service.Type) } expected := info.MemdConns[i] if service.Remote != expected.RemoteAddr { suite.T().Fatalf("Expected service Remote to be %s but was %s", expected.RemoteAddr, service.Remote) } if service.Local != expected.LocalAddr { suite.T().Fatalf("Expected service Local to be %s but was %s", expected.LocalAddr, service.Local) } if service.LastActivity != expected.LastActivity { suite.T().Fatalf("Expected service LastActivity to be %s but was %s", expected.LastActivity, service.LastActivity) } if service.Namespace != expected.Scope { suite.T().Fatalf("Expected service Scope to be %s but was %s", expected.Scope, service.Namespace) } if service.ID != expected.ID { suite.T().Fatalf("Expected service ID to be %s but was %s", expected.ID, service.ID) } if service.State != EndpointState(expected.State) { suite.T().Fatalf("Expected service state to be %s but was %s", endpointStateToString(EndpointState(expected.State)), endpointStateToString(service.State)) } } marshaled, err := json.Marshal(report) if err != nil { suite.T().Fatalf("Failed to Marshal report: %v", err) } var jsonReport jsonDiagnosticReport err = json.Unmarshal(marshaled, &jsonReport) if err != nil { suite.T().Fatalf("Failed to Unmarshal report: %v", err) } if jsonReport.ID != report.ID { suite.T().Fatalf("Expected json report ID to be %s but was %s", report.ID, jsonReport.ID) } if jsonReport.Version != 2 { suite.T().Fatalf("Expected json report Version to be 1 but was %d", jsonReport.Version) } if jsonReport.SDK != Identifier() { suite.T().Fatalf("Expected json report SDK to be %s but was %s", Identifier(), jsonReport.SDK) } if len(jsonReport.Services) != 1 { suite.T().Fatalf("Expected json report Services to be of length 1 but was %d", len(jsonReport.Services)) } jsonServices, ok := jsonReport.Services["kv"] if !ok { suite.T().Fatalf("Expected json report services to contain kv but didn't") } if len(services) != len(jsonServices) { suite.T().Fatalf("Expected json report Services length to be %d but was %d", len(report.Services), len(services)) } for i, service := range jsonServices { expected := services[i] if service.Remote != expected.Remote { suite.T().Fatalf("Expected service Remote to be %s but was %s", expected.Remote, service.Remote) } if service.Local != expected.Local { suite.T().Fatalf("Expected service Local to be %s but was %s", expected.Local, service.Local) } if service.LastActivityUs == 0 { suite.T().Fatalf("Expected service LastActivityUs to be non zero but was %d", service.LastActivityUs) } if service.Namespace != expected.Namespace { suite.T().Fatalf("Expected service Scope to be %s but was %s", expected.Namespace, service.Namespace) } if service.ID != expected.ID { suite.T().Fatalf("Expected service Scope to be %s but was %s", expected.ID, service.ID) } if service.State != endpointStateToString(expected.State) { suite.T().Fatalf("Expected service state to be %s but was %s", endpointStateToString(expected.State), service.State) } } } func (suite *UnitTestSuite) TestDiagnosticsWithID() { provider := new(mockDiagnosticsProvider) provider. On("Diagnostics", mock.AnythingOfType("gocbcore.DiagnosticsOptions")). Return(&gocbcore.DiagnosticInfo{ ConfigRev: 1, }, nil) cli := new(mockConnectionManager) cli.On("getDiagnosticsProvider", "").Return(provider, nil) c := &Cluster{ connectionManager: cli, } report, err := c.Diagnostics(&DiagnosticsOptions{ReportID: "myreportid"}) if err != nil { suite.T().Fatalf("Expected error to be nil but was %v", err) } if report.ID != "myreportid" { suite.T().Fatalf("Report ID should have been myreportid but was %s", report.ID) } } gocb-2.6.3/cluster_eventingmgr.go000066400000000000000000001101751441755043100170620ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "strings" "time" ) // EventingFunctionManager provides methods for performing eventing function management operations. // This manager is designed to work only against Couchbase Server 7.0+, it might work against earlier server // versions but that is not tested and is not supported. // UNCOMMITTED: This API may change in the future. type EventingFunctionManager struct { mgmtProvider mgmtProvider tracer RequestTracer meter *meterWrapper } func (efm *EventingFunctionManager) doMgmtRequest(ctx context.Context, req mgmtRequest) (*mgmtResponse, error) { resp, err := efm.mgmtProvider.executeMgmtRequest(ctx, req) if err != nil { return nil, err } return resp, nil } func (efm *EventingFunctionManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read eventing function response body: %s", err) return nil } var baseErr error strBody := string(b) if strings.Contains(strBody, "ERR_APP_NOT_FOUND_TS") { baseErr = ErrEventingFunctionNotFound } else if strings.Contains(strBody, "ERR_APP_NOT_DEPLOYED") { baseErr = ErrEventingFunctionNotDeployed } else if strings.Contains(strBody, "ERR_HANDLER_COMPILATION") { baseErr = ErrEventingFunctionCompilationFailure } else if strings.Contains(strBody, "ERR_SRC_MB_SAME") { baseErr = ErrEventingFunctionIdenticalKeyspace } else if strings.Contains(strBody, "ERR_APP_NOT_BOOTSTRAPPED") { baseErr = ErrEventingFunctionNotBootstrapped } else if strings.Contains(strBody, "ERR_APP_NOT_UNDEPLOYED") { baseErr = ErrEventingFunctionDeployed } else if strings.Contains(strBody, "ERR_COLLECTION_MISSING") { baseErr = ErrCollectionNotFound } else if strings.Contains(strBody, "ERR_BUCKET_MISSING") { baseErr = ErrBucketNotFound } else { baseErr = errors.New(string(b)) } return makeGenericMgmtError(baseErr, req, resp, strBody) } type jsonEventingFunction struct { Name string `json:"appname"` Code string `json:"appcode"` Version string `json:"version"` EnforceSchema bool `json:"enforce_schema,omitempty"` HandlerUUID int `json:"handleruuid,omitempty"` FunctionInstanceID string `json:"function_instance_id,omitempty"` Settings jsonEventingFunctionSettings `json:"settings"` DeploymentConfig jsonEventingFunctionDeploymentConfig `json:"depcfg"` } type jsonEventingFunctionSettings struct { CPPWorkerThreadCount int `json:"cpp_worker_thread_count,omitempty"` DCPStreamBoundary EventingFunctionDCPBoundary `json:"dcp_stream_boundary,omitempty"` Description string `json:"description,omitempty"` DeploymentStatus EventingFunctionDeploymentStatus `json:"deployment_status"` ProcessingStatus EventingFunctionProcessingStatus `json:"processing_status"` LanguageCompatibility EventingFunctionLanguageCompatibility `json:"language_compatibility,omitempty"` LogLevel EventingFunctionLogLevel `json:"log_level,omitempty"` ExecutionTimeout int `json:"execution_timeout,omitempty"` LCBInstCapacity int `json:"lcb_inst_capacity,omitempty"` LCBRetryCount int `json:"lcb_retry_count,omitempty"` LCBTimeout int `json:"lcb_timeout,omitempty"` QueryConsistency QueryScanConsistency `json:"n1ql_consistency,omitempty"` NumTimerPartitions int `json:"num_timer_partitions,omitempty"` SockBatchSize int `json:"sock_batch_size,omitempty"` TickDuration int `json:"tick_duration,omitempty"` TimerContextSize int `json:"timer_context_size,omitempty"` UserPrefix string `json:"user_prefix,omitempty"` BucketCacheSize int `json:"bucket_cache_size,omitempty"` BucketCacheAge int `json:"bucket_cache_age,omitempty"` CurlMaxAllowedRespSize int `json:"curl_max_allowed_resp_size,omitempty"` QueryPrepareAll bool `json:"n1ql_prepare_all,omitempty"` WorkerCount int `json:"worker_count,omitempty"` HandlerHeaders []string `json:"handler_headers,omitempty"` HandlerFooters []string `json:"handler_footers,omitempty"` EnableAppLogRotation bool `json:"enable_applog_rotation,omitempty"` AppLogDir string `json:"app_log_dir,omitempty"` AppLogMaxSize int `json:"app_log_max_size,omitempty"` AppLogMaxFiles int `json:"app_log_max_files,omitempty"` CheckpointInterval int `json:"checkpoint_interval,omitempty"` } type jsonEventingFunctionDeploymentConfig struct { MetadataBucket string `json:"metadata_bucket"` MetadataScope string `json:"metadata_scope,omitempty"` MetadataCollection string `json:"metadata_collection,omitempty"` SourceBucket string `json:"source_bucket"` SourceScope string `json:"source_scope,omitempty"` SourceCollection string `json:"source_collection,omitempty"` BucketBindings []jsonEventingFunctionBucketBinding `json:"buckets,omitempty"` UrlBindings []jsonEventingFunctionUrlBinding `json:"curl,omitempty"` ConstantBindings []jsonEventingFunctionConstantBinding `json:"constants,omitempty"` } type jsonEventingFunctionBucketBinding struct { Alias string `json:"alias"` Bucket string `json:"bucket_name"` Scope string `json:"scope_name,omitempty"` Collection string `json:"collection_name,omitempty"` Access EventingFunctionBucketAccess `json:"access"` } type jsonEventingFunctionUrlBinding struct { Hostname string `json:"hostname"` Alias string `json:"value"` AuthType string `json:"auth_type"` AllowCookies bool `json:"allow_cookies"` ValidateSSLCertificate bool `json:"validate_ssl_certificate"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` BearerKey string `json:"bearer_key,omitempty"` } type jsonEventingFunctionConstantBinding struct { Alias string `json:"value"` Literal string `json:"literal"` } type jsonEventingFunctionsStatusApp struct { CompositeStatus EventingFunctionStatus `json:"composite_status"` Name string `json:"name"` NumBootstrappingNodes int `json:"num_bootstrapping_nodes"` NumDeployedNodes int `json:"num_deployed_nodes"` DeploymentStatus EventingFunctionDeploymentStatus `json:"deployment_status"` ProcessingStatus EventingFunctionProcessingStatus `json:"processing_status"` } type jsonEventingFunctionsStatus struct { Apps []jsonEventingFunctionsStatusApp `json:"apps"` NumEventingNodes int `json:"num_eventing_nodes"` } // EventingFunctionStatus describes the current state of an eventing function. type EventingFunctionStatus string var ( // EventingFunctionStateUndeployed represents that the eventing function is undeployed. EventingFunctionStateUndeployed EventingFunctionStatus = "undeployed" // EventingFunctionStateDeploying represents that the eventing function is deploying. EventingFunctionStateDeploying EventingFunctionStatus = "deploying" // EventingFunctionStateDeployed represents that the eventing function is deployed. EventingFunctionStateDeployed EventingFunctionStatus = "deployed" // EventingFunctionStateUndeploying represents that the eventing function is undeploying. EventingFunctionStateUndeploying EventingFunctionStatus = "undeploying" // EventingFunctionStatePaused represents that the eventing function is paused. EventingFunctionStatePaused EventingFunctionStatus = "paused" // EventingFunctionStatePausing represents that the eventing function is pausing. EventingFunctionStatePausing EventingFunctionStatus = "pausing" ) // EventingFunctionDCPBoundary sets what data mutations to deploy the eventing function for. type EventingFunctionDCPBoundary string var ( // EventingFunctionDCPBoundaryEverything will deploy the eventing function for all data mutations. EventingFunctionDCPBoundaryEverything EventingFunctionDCPBoundary = "everything" // EventingFunctionDCPBoundaryFromNow will deploy the eventing function for only data mutations occurring post deployment. EventingFunctionDCPBoundaryFromNow EventingFunctionDCPBoundary = "from_now" ) // EventingFunctionDeploymentStatus represents the current deployment status for the eventing function. type EventingFunctionDeploymentStatus bool var ( // EventingFunctionDeploymentStatusDeployed represents that the eventing function is currently deployed. EventingFunctionDeploymentStatusDeployed EventingFunctionDeploymentStatus = true // EventingFunctionDeploymentStatusUndeployed represents that the eventing function is currently undeployed. EventingFunctionDeploymentStatusUndeployed EventingFunctionDeploymentStatus = false ) // EventingFunctionProcessingStatus represents the current processing status for the eventing function. type EventingFunctionProcessingStatus bool var ( // EventingFunctionProcessingStatusRunning represents that the eventing function is currently running. EventingFunctionProcessingStatusRunning EventingFunctionProcessingStatus = true // EventingFunctionProcessingStatusPaused represents that the eventing function is currently paused. EventingFunctionProcessingStatusPaused EventingFunctionProcessingStatus = false ) // EventingFunctionLanguageCompatibility represents the eventing function language compatibility for backward compatibility. type EventingFunctionLanguageCompatibility string var ( // EventingFunctionLanguageCompatibilityVersion600 represents the eventing function language compatibility 6.0.0. EventingFunctionLanguageCompatibilityVersion600 EventingFunctionLanguageCompatibility = "6.0.0" // EventingFunctionLanguageCompatibilityVersion650 represents the eventing function language compatibility 6.5.0. EventingFunctionLanguageCompatibilityVersion650 EventingFunctionLanguageCompatibility = "6.5.0" // EventingFunctionLanguageCompatibilityVersion662 represents the eventing function language compatibility 6.6.2. EventingFunctionLanguageCompatibilityVersion662 EventingFunctionLanguageCompatibility = "6.6.2" ) // EventingFunctionLogLevel represents the granularity at which to log messages for the eventing function. type EventingFunctionLogLevel string var ( // EventingFunctionLogLevelInfo represents to log messages at INFO for the eventing function. EventingFunctionLogLevelInfo EventingFunctionLogLevel = "INFO" // EventingFunctionLogLevelError represents to log messages at ERROR for the eventing function. EventingFunctionLogLevelError EventingFunctionLogLevel = "ERROR" // EventingFunctionLogLevelWarning represents to log messages at WARNING for the eventing function. EventingFunctionLogLevelWarning EventingFunctionLogLevel = "WARNING" // EventingFunctionLogLevelDebug represents to log messages at DEBUG for the eventing function. EventingFunctionLogLevelDebug EventingFunctionLogLevel = "DEBUG" // EventingFunctionLogLevelTrace represents to log messages at TRACE for the eventing function. EventingFunctionLogLevelTrace EventingFunctionLogLevel = "TRACE" ) // EventingFunctionSettings are the settings for an EventingFunction. type EventingFunctionSettings struct { CPPWorkerThreadCount int DCPStreamBoundary EventingFunctionDCPBoundary Description string DeploymentStatus EventingFunctionDeploymentStatus ProcessingStatus EventingFunctionProcessingStatus LanguageCompatibility EventingFunctionLanguageCompatibility LogLevel EventingFunctionLogLevel ExecutionTimeout time.Duration LCBInstCapacity int LCBRetryCount int LCBTimeout time.Duration QueryConsistency QueryScanConsistency NumTimerPartitions int SockBatchSize int TickDuration time.Duration TimerContextSize int UserPrefix string BucketCacheSize int BucketCacheAge int CurlMaxAllowedRespSize int QueryPrepareAll bool WorkerCount int HandlerHeaders []string HandlerFooters []string EnableAppLogRotation bool AppLogDir string AppLogMaxSize int AppLogMaxFiles int CheckpointInterval time.Duration } // EventingFunctionBucketAccess represents the level of access an eventing function has to a bucket. type EventingFunctionBucketAccess string var ( // EventingFunctionBucketAccessReadOnly represents readonly access to a bucket for an eventing function. EventingFunctionBucketAccessReadOnly EventingFunctionBucketAccess = "r" // EventingFunctionBucketAccessReadWrite represents readwrite access to a bucket for an eventing function. EventingFunctionBucketAccessReadWrite EventingFunctionBucketAccess = "rw" ) // EventingFunctionUrlAuth represents an authentication method for EventingFunctionUrlBinding for an eventing function. type EventingFunctionUrlAuth interface { Method() string Username() string Password() string Key() string } // EventingFunctionUrlNoAuth specifies that no authentication is used for the EventingFunctionUrlBinding. type EventingFunctionUrlNoAuth struct{} func (ua EventingFunctionUrlNoAuth) Method() string { return "no-auth" } func (ua EventingFunctionUrlNoAuth) Username() string { return "" } func (ua EventingFunctionUrlNoAuth) Password() string { return "" } func (ua EventingFunctionUrlNoAuth) Key() string { return "" } // EventingFunctionUrlAuthBasic specifies that basic authentication is used for the EventingFunctionUrlBinding. type EventingFunctionUrlAuthBasic struct { User string Pass string } func (ua EventingFunctionUrlAuthBasic) Method() string { return "basic" } func (ua EventingFunctionUrlAuthBasic) Username() string { return ua.User } func (ua EventingFunctionUrlAuthBasic) Password() string { return ua.Pass } func (ua EventingFunctionUrlAuthBasic) Key() string { return "" } // EventingFunctionUrlAuthDigest specifies that digest authentication is used for the EventingFunctionUrlBinding. type EventingFunctionUrlAuthDigest struct { User string Pass string } func (ua EventingFunctionUrlAuthDigest) Method() string { return "digest" } func (ua EventingFunctionUrlAuthDigest) Username() string { return ua.User } func (ua EventingFunctionUrlAuthDigest) Password() string { return ua.Pass } func (ua EventingFunctionUrlAuthDigest) Key() string { return "" } // EventingFunctionUrlAuthBearer specifies that bearer token authentication is used for the EventingFunctionUrlBinding. type EventingFunctionUrlAuthBearer struct { BearerKey string } func (ua EventingFunctionUrlAuthBearer) Method() string { return "bearer" } func (ua EventingFunctionUrlAuthBearer) Username() string { return "" } func (ua EventingFunctionUrlAuthBearer) Password() string { return "" } func (ua EventingFunctionUrlAuthBearer) Key() string { return ua.BearerKey } // EventingFunctionBucketBinding represents an eventing function binding allowing the function access to buckets, // scopes, and collections. type EventingFunctionBucketBinding struct { Alias string Name EventingFunctionKeyspace Access EventingFunctionBucketAccess } // EventingFunctionUrlBinding represents an eventing function binding allowing the function access external resources // via cURL. type EventingFunctionUrlBinding struct { Hostname string Alias string Auth EventingFunctionUrlAuth AllowCookies bool ValidateSSLCertificate bool } // EventingFunctionConstantBinding represents an eventing function binding allowing the function to utilize global variables. type EventingFunctionConstantBinding struct { Alias string Literal string } // EventingFunctionKeyspace represents a triple of bucket, collection, and scope names. type EventingFunctionKeyspace struct { Bucket string Scope string Collection string } // EventingStatus represents the current state of all eventing functions. type EventingStatus struct { NumEventingNodes int Functions []EventingFunctionState } // EventingFunctionState represents the current state of an eventing function. type EventingFunctionState struct { Name string Status EventingFunctionStatus NumBootstrappingNodes int NumDeployedNodes int DeploymentStatus EventingFunctionDeploymentStatus ProcessingStatus EventingFunctionProcessingStatus } func (ef *EventingStatus) UnmarshalJSON(b []byte) error { var jf jsonEventingFunctionsStatus err := json.Unmarshal(b, &jf) if err != nil { return err } var funcs []EventingFunctionState for _, f := range jf.Apps { funcs = append(funcs, EventingFunctionState{ Name: f.Name, Status: f.CompositeStatus, NumBootstrappingNodes: f.NumBootstrappingNodes, NumDeployedNodes: f.NumDeployedNodes, DeploymentStatus: f.DeploymentStatus, ProcessingStatus: f.ProcessingStatus, }) } ef.NumEventingNodes = jf.NumEventingNodes ef.Functions = funcs return nil } // EventingFunction represents an eventing function. type EventingFunction struct { Name string // Required Code string // Required Version string EnforceSchema bool HandlerUUID int FunctionInstanceID string MetadataKeyspace EventingFunctionKeyspace // Required SourceKeyspace EventingFunctionKeyspace // Required BucketBindings []EventingFunctionBucketBinding UrlBindings []EventingFunctionUrlBinding ConstantBindings []EventingFunctionConstantBinding Settings EventingFunctionSettings } func (ef EventingFunction) MarshalJSON() ([]byte, error) { var bucketBindings []jsonEventingFunctionBucketBinding for _, b := range ef.BucketBindings { bucketBindings = append(bucketBindings, jsonEventingFunctionBucketBinding{ Alias: b.Alias, Bucket: b.Name.Bucket, Scope: b.Name.Scope, Collection: b.Name.Collection, Access: b.Access, }) } var urlBindings []jsonEventingFunctionUrlBinding for _, b := range ef.UrlBindings { urlBindings = append(urlBindings, jsonEventingFunctionUrlBinding{ Hostname: b.Hostname, Alias: b.Alias, AuthType: b.Auth.Method(), AllowCookies: b.AllowCookies, ValidateSSLCertificate: b.ValidateSSLCertificate, Username: b.Auth.Username(), Password: b.Auth.Password(), BearerKey: b.Auth.Key(), }) } var constantBindings []jsonEventingFunctionConstantBinding for _, b := range ef.ConstantBindings { constantBindings = append(constantBindings, jsonEventingFunctionConstantBinding(b)) } jsonSettings := jsonEventingFunction{ Name: ef.Name, Code: ef.Code, Version: ef.Version, EnforceSchema: ef.EnforceSchema, HandlerUUID: ef.HandlerUUID, FunctionInstanceID: ef.FunctionInstanceID, Settings: jsonEventingFunctionSettings{ CPPWorkerThreadCount: ef.Settings.CPPWorkerThreadCount, DCPStreamBoundary: ef.Settings.DCPStreamBoundary, Description: ef.Settings.Description, DeploymentStatus: ef.Settings.DeploymentStatus, ProcessingStatus: ef.Settings.ProcessingStatus, LanguageCompatibility: ef.Settings.LanguageCompatibility, LogLevel: ef.Settings.LogLevel, ExecutionTimeout: int(ef.Settings.ExecutionTimeout.Seconds()), LCBInstCapacity: ef.Settings.LCBInstCapacity, LCBRetryCount: ef.Settings.LCBRetryCount, LCBTimeout: int(ef.Settings.LCBTimeout.Seconds()), QueryConsistency: ef.Settings.QueryConsistency, NumTimerPartitions: ef.Settings.NumTimerPartitions, SockBatchSize: ef.Settings.SockBatchSize, TickDuration: int(ef.Settings.TickDuration.Milliseconds()), TimerContextSize: ef.Settings.TimerContextSize, UserPrefix: ef.Settings.UserPrefix, BucketCacheSize: ef.Settings.BucketCacheSize, BucketCacheAge: ef.Settings.BucketCacheAge, CurlMaxAllowedRespSize: ef.Settings.CurlMaxAllowedRespSize, QueryPrepareAll: ef.Settings.QueryPrepareAll, WorkerCount: ef.Settings.WorkerCount, HandlerHeaders: ef.Settings.HandlerHeaders, HandlerFooters: ef.Settings.HandlerFooters, EnableAppLogRotation: ef.Settings.EnableAppLogRotation, AppLogDir: ef.Settings.AppLogDir, AppLogMaxSize: ef.Settings.AppLogMaxSize, AppLogMaxFiles: ef.Settings.AppLogMaxFiles, CheckpointInterval: int(ef.Settings.CheckpointInterval.Seconds()), }, DeploymentConfig: jsonEventingFunctionDeploymentConfig{ MetadataBucket: ef.MetadataKeyspace.Bucket, MetadataScope: ef.MetadataKeyspace.Scope, MetadataCollection: ef.MetadataKeyspace.Collection, SourceBucket: ef.SourceKeyspace.Bucket, SourceScope: ef.SourceKeyspace.Scope, SourceCollection: ef.SourceKeyspace.Collection, BucketBindings: bucketBindings, UrlBindings: urlBindings, ConstantBindings: constantBindings, }, } return json.Marshal(jsonSettings) } func (ef *EventingFunction) UnmarshalJSON(b []byte) error { var jf jsonEventingFunction err := json.Unmarshal(b, &jf) if err != nil { return err } var bucketBindings []EventingFunctionBucketBinding for _, b := range jf.DeploymentConfig.BucketBindings { bucketBindings = append(bucketBindings, EventingFunctionBucketBinding{ Alias: b.Alias, Name: EventingFunctionKeyspace{ Bucket: b.Bucket, Scope: b.Scope, Collection: b.Collection, }, Access: b.Access, }) } var urlBindings []EventingFunctionUrlBinding for _, b := range jf.DeploymentConfig.UrlBindings { var auth EventingFunctionUrlAuth switch b.AuthType { case "no-auth": auth = EventingFunctionUrlNoAuth{} case "basic": auth = EventingFunctionUrlAuthBasic{ User: b.Username, } case "digest": auth = EventingFunctionUrlAuthDigest{ User: b.Username, } case "bearer": auth = EventingFunctionUrlAuthBearer{} } urlBindings = append(urlBindings, EventingFunctionUrlBinding{ Hostname: b.Hostname, Alias: b.Alias, Auth: auth, AllowCookies: b.AllowCookies, ValidateSSLCertificate: b.ValidateSSLCertificate, }) } var constantBindings []EventingFunctionConstantBinding for _, b := range jf.DeploymentConfig.ConstantBindings { constantBindings = append(constantBindings, EventingFunctionConstantBinding(b)) } ef.Name = jf.Name ef.Code = jf.Code ef.Version = jf.Version ef.EnforceSchema = jf.EnforceSchema ef.HandlerUUID = jf.HandlerUUID ef.FunctionInstanceID = jf.FunctionInstanceID ef.Settings = EventingFunctionSettings{ CPPWorkerThreadCount: jf.Settings.CPPWorkerThreadCount, DCPStreamBoundary: jf.Settings.DCPStreamBoundary, Description: jf.Settings.Description, DeploymentStatus: jf.Settings.DeploymentStatus, ProcessingStatus: jf.Settings.ProcessingStatus, LanguageCompatibility: jf.Settings.LanguageCompatibility, LogLevel: jf.Settings.LogLevel, ExecutionTimeout: time.Duration(jf.Settings.ExecutionTimeout) * time.Second, LCBInstCapacity: jf.Settings.LCBInstCapacity, LCBRetryCount: jf.Settings.LCBRetryCount, LCBTimeout: time.Duration(jf.Settings.LCBTimeout) * time.Second, QueryConsistency: jf.Settings.QueryConsistency, NumTimerPartitions: jf.Settings.NumTimerPartitions, SockBatchSize: jf.Settings.SockBatchSize, TickDuration: time.Duration(jf.Settings.TickDuration) * time.Millisecond, TimerContextSize: jf.Settings.TimerContextSize, UserPrefix: jf.Settings.UserPrefix, BucketCacheSize: jf.Settings.BucketCacheSize, BucketCacheAge: jf.Settings.BucketCacheAge, CurlMaxAllowedRespSize: jf.Settings.CurlMaxAllowedRespSize, QueryPrepareAll: jf.Settings.QueryPrepareAll, WorkerCount: jf.Settings.WorkerCount, HandlerHeaders: jf.Settings.HandlerHeaders, HandlerFooters: jf.Settings.HandlerFooters, EnableAppLogRotation: jf.Settings.EnableAppLogRotation, AppLogDir: jf.Settings.AppLogDir, AppLogMaxSize: jf.Settings.AppLogMaxSize, AppLogMaxFiles: jf.Settings.AppLogMaxFiles, CheckpointInterval: time.Duration(jf.Settings.CheckpointInterval) * time.Second, } ef.MetadataKeyspace = EventingFunctionKeyspace{ Bucket: jf.DeploymentConfig.MetadataBucket, Scope: jf.DeploymentConfig.MetadataScope, Collection: jf.DeploymentConfig.MetadataCollection, } ef.SourceKeyspace = EventingFunctionKeyspace{ Bucket: jf.DeploymentConfig.SourceBucket, Scope: jf.DeploymentConfig.SourceScope, Collection: jf.DeploymentConfig.SourceCollection, } ef.BucketBindings = bucketBindings ef.UrlBindings = urlBindings ef.ConstantBindings = constantBindings return nil } type eventingRequestOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan Context context.Context } func (efm *EventingFunctionManager) doRequest(path string, method string, opName string, function *EventingFunction, target interface{}, opts eventingRequestOptions) error { start := time.Now() defer efm.meter.ValueRecord(meterValueServiceManagement, opName, start) op := "manager_eventing_" + opName span := createSpan(efm.tracer, opts.ParentSpan, op, "management") span.SetAttribute("db.operation", method+" "+path) defer span.End() var b []byte if function != nil { var err error b, err = json.Marshal(function) if err != nil { return err } } req := mgmtRequest{ Service: ServiceTypeEventing, Method: method, Path: path, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), Body: b, } resp, err := efm.doMgmtRequest(opts.Context, req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := efm.tryParseErrorMessage(&req, resp) if idxErr != nil { return idxErr } return makeMgmtBadStatusError("failed eventing "+opName, &req, resp) } if target != nil { jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(target) if err != nil { return err } } return nil } // UpsertEventingFunctionOptions are the options available when using the UpsertFunction operation. type UpsertEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UpsertFunction inserts or updates an eventing function. func (efm *EventingFunctionManager) UpsertFunction(function EventingFunction, opts *UpsertEventingFunctionOptions) error { if opts == nil { opts = &UpsertEventingFunctionOptions{} } return efm.doRequest(fmt.Sprintf("/api/v1/functions/%s", function.Name), "POST", "upsert_function", &function, nil, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // DropEventingFunctionOptions are the options available when using the DropFunction operation. type DropEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropFunction drops an eventing function. func (efm *EventingFunctionManager) DropFunction(name string, opts *DropEventingFunctionOptions) error { if opts == nil { opts = &DropEventingFunctionOptions{} } return efm.doRequest(fmt.Sprintf("/api/v1/functions/%s", name), "DELETE", "drop_function", nil, nil, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // DeployEventingFunctionOptions are the options available when using the DeployFunction operation. type DeployEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DeployFunction deploys an eventing function. func (efm *EventingFunctionManager) DeployFunction(name string, opts *DeployEventingFunctionOptions) error { if opts == nil { opts = &DeployEventingFunctionOptions{} } return efm.doRequest(fmt.Sprintf("/api/v1/functions/%s/deploy", name), "POST", "deploy_function", nil, nil, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // UndeployEventingFunctionOptions are the options available when using the UndeployFunction operation. type UndeployEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UndeployFunction undeploys an eventing function. func (efm *EventingFunctionManager) UndeployFunction(name string, opts *UndeployEventingFunctionOptions) error { if opts == nil { opts = &UndeployEventingFunctionOptions{} } return efm.doRequest(fmt.Sprintf("/api/v1/functions/%s/undeploy", name), "POST", "undeploy_function", nil, nil, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // GetAllEventingFunctionsOptions are the options available when using the GetAllFunctions operation. type GetAllEventingFunctionsOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllFunctions fetches all of the eventing functions. func (efm *EventingFunctionManager) GetAllFunctions(opts *GetAllEventingFunctionsOptions) ([]EventingFunction, error) { if opts == nil { opts = &GetAllEventingFunctionsOptions{} } var functions []EventingFunction err := efm.doRequest("/api/v1/functions", "GET", "get_all_functions", nil, &functions, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) if err != nil { return nil, err } return functions, nil } // GetEventingFunctionOptions are the options available when using the GetFunction operation. type GetEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetFunction fetches an eventing function. func (efm *EventingFunctionManager) GetFunction(name string, opts *GetEventingFunctionOptions) (*EventingFunction, error) { if opts == nil { opts = &GetEventingFunctionOptions{} } var function *EventingFunction err := efm.doRequest(fmt.Sprintf("/api/v1/functions/%s", name), "GET", "get_function", nil, &function, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) if err != nil { return nil, err } return function, nil } // PauseEventingFunctionOptions are the options available when using the PauseFunction operation. type PauseEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // PauseFunction pauses an eventing function. func (efm *EventingFunctionManager) PauseFunction(name string, opts *PauseEventingFunctionOptions) error { if opts == nil { opts = &PauseEventingFunctionOptions{} } return efm.doRequest(fmt.Sprintf("/api/v1/functions/%s/pause", name), "POST", "pause_function", nil, nil, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // ResumeEventingFunctionOptions are the options available when using the ResumeFunction operation. type ResumeEventingFunctionOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // ResumeFunction resumes an eventing function. func (efm *EventingFunctionManager) ResumeFunction(name string, opts *ResumeEventingFunctionOptions) error { if opts == nil { opts = &ResumeEventingFunctionOptions{} } return efm.doRequest(fmt.Sprintf("/api/v1/functions/%s/resume", name), "POST", "resume_function", nil, nil, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // EventingFunctionsStatusOptions are the options available when using the FunctionsStatus operation. type EventingFunctionsStatusOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // FunctionsStatus fetches the current status of all eventing functions. func (efm *EventingFunctionManager) FunctionsStatus(opts *EventingFunctionsStatusOptions) (*EventingStatus, error) { if opts == nil { opts = &EventingFunctionsStatusOptions{} } var functions *EventingStatus err := efm.doRequest("/api/v1/status", "GET", "functions_status", nil, &functions, eventingRequestOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) if err != nil { return nil, err } return functions, nil } gocb-2.6.3/cluster_eventingmgr_test.go000066400000000000000000000446061441755043100201260ustar00rootroot00000000000000package gocb import ( "errors" "fmt" "github.com/google/uuid" "time" ) func (suite *IntegrationTestSuite) TestEventingManagerUpsertGetDrop() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustCreateCollection(scopeName, "meta") suite.mustWaitForEventingCollections(scopeName, []string{"source", "meta"}) fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `function OnUpdate(doc, meta) { }`, BucketBindings: []EventingFunctionBucketBinding{ { Name: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: globalScope.Name(), Collection: globalCollection.Name(), }, Alias: "bucketbinding1", Access: EventingFunctionBucketAccessReadWrite, }, }, UrlBindings: []EventingFunctionUrlBinding{ { Hostname: "http://127.0.0.1", Alias: "urlbinding1", Auth: EventingFunctionUrlAuthBasic{ User: "dave", Pass: "password", }, AllowCookies: false, ValidateSSLCertificate: false, }, }, ConstantBindings: []EventingFunctionConstantBinding{ { Alias: "someconstant", Literal: "someliteral", }, }, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "meta", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, } success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { err := mgr.UpsertFunction(expectedFn, nil) if err != nil { suite.T().Logf("Upsert function failed: %v", err) return false } return true }) suite.Require().True(success, "Upsert function did not succeed in time") functions, err := mgr.GetAllFunctions(nil) suite.Require().Nil(err, err) var found bool for _, fn := range functions { if fn.Name == fnName { found = true suite.Assert().Equal(expectedFn.Code, fn.Code) } } suite.Assert().True(found, fmt.Sprintf("Eventing function %s not found in GetAllFunctions", fnName)) funcsStatus, err := mgr.FunctionsStatus(nil) suite.Require().Nil(err) var foundStatus *EventingFunctionState for _, fn := range funcsStatus.Functions { if fn.Name == fnName { foundStatus = &fn } } suite.Assert().NotZero(foundStatus.Status) actualFn, err := mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Assert().Equal(expectedFn.Code, actualFn.Code) err = mgr.DropFunction(fnName, nil) suite.Require().Nil(err) } func (suite *IntegrationTestSuite) TestEventingManagerUnknownBucket() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) suite.skipIfUnsupported(EventingFunctionManagerMB52572Feature) mgr := globalCluster.Cluster.EventingFunctions() fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `feefifofum`, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: "immadeup", Scope: "idontexist", Collection: "meeither", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: "immadeup", Scope: "idontexist2", Collection: "meeither2", }, } err := mgr.UpsertFunction(expectedFn, nil) if !errors.Is(err, ErrBucketNotFound) { suite.T().Logf("Expected ResumeFunction to fail with bucket not found but was %v", err) suite.T().Fail() } } func (suite *IntegrationTestSuite) TestEventingManagerUnknownFunction() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustCreateCollection(scopeName, "meta") suite.mustWaitForEventingCollections(scopeName, []string{"source", "meta"}) fnName := uuid.New().String() fn, err := mgr.GetFunction(fnName, nil) suite.Assert().Nil(fn) if !errors.Is(err, ErrEventingFunctionNotFound) { suite.T().Logf("Expected GetFunction to fail with not found but was %v", err) suite.T().Fail() } err = mgr.DeployFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotFound) { suite.T().Logf("Expected DeployFunction to fail with not found but was %v", err) suite.T().Fail() } err = mgr.PauseFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotFound) { suite.T().Logf("Expected PauseFunction to fail with not found but was %v", err) suite.T().Fail() } // see MB-47840 on why those are not only ErrEventingFunctionNotFound err = mgr.DropFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotDeployed) && !errors.Is(err, ErrEventingFunctionNotFound) { suite.T().Logf("Expected DropFunction to fail with not deployed but was %v", err) suite.T().Fail() } err = mgr.UndeployFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotDeployed) && !errors.Is(err, ErrEventingFunctionNotFound) { suite.T().Logf("Expected UndeployFunction to fail with not deployed but was %v", err) suite.T().Fail() } err = mgr.ResumeFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotDeployed) && !errors.Is(err, ErrEventingFunctionNotFound) { suite.T().Logf("Expected ResumeFunction to fail with not deployed but was %v", err) suite.T().Fail() } } func (suite *IntegrationTestSuite) TestEventingManagerInvalidCode() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustCreateCollection(scopeName, "meta") suite.mustWaitForEventingCollections(scopeName, []string{"source", "meta"}) fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `feefifofum`, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "meta", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, } success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { err := mgr.UpsertFunction(expectedFn, nil) if !errors.Is(err, ErrEventingFunctionCompilationFailure) { suite.T().Logf("Expected ResumeFunction to fail with compilation failure but was %v", err) return false } return true }) suite.Require().True(success, "Upsert function did not fail in the expected way in time") } func (suite *IntegrationTestSuite) TestEventingManagerCollectionNotFound() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustWaitForEventingCollections(scopeName, []string{"source"}) fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `feefifofum`, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "idefinitelydontexist", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, } err := mgr.UpsertFunction(expectedFn, nil) if !errors.Is(err, ErrCollectionNotFound) { suite.T().Logf("Expected ResumeFunction to fail with collection not found but was %v", err) suite.T().Fail() } } func (suite *IntegrationTestSuite) TestEventingManagerSameSourceAndMetaKeyspace() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustWaitForEventingCollections(scopeName, []string{"source"}) fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `feefifofum`, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, } success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { err := mgr.UpsertFunction(expectedFn, nil) if !errors.Is(err, ErrEventingFunctionIdenticalKeyspace) { suite.T().Logf("Expected ResumeFunction to fail with identical keyspace but was %v", err) return false } return true }) suite.Require().True(success, "Upsert function did not fail in the expected way in time") } func (suite *IntegrationTestSuite) TestEventingManagerDeploysAndUndeploys() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) suite.skipIfUnsupported(EventingFunctionManagerMB52649Feature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustCreateCollection(scopeName, "meta") suite.mustWaitForEventingCollections(scopeName, []string{"source", "meta"}) fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `function OnUpdate(doc, meta) { }`, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "meta", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, } success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { err := mgr.UpsertFunction(expectedFn, nil) if err != nil { suite.T().Logf("Expected UpsertFunction to succeed: %v", err) return false } return true }) suite.Require().True(success, "Upsert function did not succeed in time") actualFn, err := mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Require().Equal(EventingFunctionDeploymentStatusUndeployed, actualFn.Settings.DeploymentStatus) err = mgr.UndeployFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotDeployed) { suite.T().Fatalf("Expected UndeployFunction to fail with not deployed but was %v", err) } err = mgr.DeployFunction(fnName, nil) suite.Require().Nil(err, err) actualFn, err = mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Require().Equal(EventingFunctionDeploymentStatusDeployed, actualFn.Settings.DeploymentStatus) success = suite.tryUntil(time.Now().Add(60*time.Second), 500*time.Millisecond, func() bool { funcsStatus, err := mgr.FunctionsStatus(nil) suite.Require().Nil(err) for _, fn := range funcsStatus.Functions { if fn.Name == fnName { if fn.Status != EventingFunctionStateDeployed { suite.T().Logf("FunctionsStatus reports function not deployed: %s", fn.Status) } return fn.Status == EventingFunctionStateDeployed } } suite.T().Fatalf("Function not found from FunctionsStatus") return false }) suite.Require().True(success, "FunctionsStatus never reported function deployed") err = mgr.UndeployFunction(fnName, nil) suite.Require().Nil(err, err) actualFn, err = mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Assert().Equal(EventingFunctionDeploymentStatusUndeployed, actualFn.Settings.DeploymentStatus) success = suite.tryUntil(time.Now().Add(60*time.Second), 500*time.Millisecond, func() bool { funcsStatus, err := mgr.FunctionsStatus(nil) suite.Require().Nil(err) for _, fn := range funcsStatus.Functions { if fn.Name == fnName { if fn.Status != EventingFunctionStateUndeployed { suite.T().Logf("FunctionsStatus reports function not undeployed: %s", fn.Status) } return fn.Status == EventingFunctionStateUndeployed } } suite.T().Fatalf("Function not found from FunctionsStatus") return false }) suite.Require().True(success, "FunctionsStatus never reported function undeployed") err = mgr.DropFunction(fnName, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) TestEventingManagerPausesAndResumes() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(EventingFunctionManagerFeature) suite.skipIfUnsupported(EventingFunctionManagerMB52649Feature) mgr := globalCluster.Cluster.EventingFunctions() scopeName := uuid.NewString() suite.mustCreateScope(scopeName) defer suite.dropScope(scopeName) suite.mustCreateCollection(scopeName, "source") suite.mustCreateCollection(scopeName, "meta") suite.mustWaitForEventingCollections(scopeName, []string{"source", "meta"}) fnName := uuid.New().String() expectedFn := EventingFunction{ Name: fnName, Code: `function OnUpdate(doc, meta) { }`, MetadataKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "meta", }, SourceKeyspace: EventingFunctionKeyspace{ Bucket: globalBucket.Name(), Scope: scopeName, Collection: "source", }, } success := suite.tryUntil(time.Now().Add(2*time.Second), 100*time.Millisecond, func() bool { err := mgr.UpsertFunction(expectedFn, nil) if err != nil { suite.T().Logf("Expected UpsertFunction to succeed: %v", err) return false } return true }) suite.Require().True(success, "Upsert function did not succeed in time") actualFn, err := mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Require().Equal(EventingFunctionProcessingStatusPaused, actualFn.Settings.ProcessingStatus) err = mgr.PauseFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotBootstrapped) { suite.T().Fatalf("Expected UndeployFunction to fail with not bootstrapped but was %v", err) } err = mgr.ResumeFunction(fnName, nil) if !errors.Is(err, ErrEventingFunctionNotDeployed) { suite.T().Fatalf("Expected UndeployFunction to fail with not deployed but was %v", err) } err = mgr.DeployFunction(fnName, nil) suite.Require().Nil(err, err) actualFn, err = mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Require().Equal(EventingFunctionProcessingStatusRunning, actualFn.Settings.ProcessingStatus) success = suite.tryUntil(time.Now().Add(60*time.Second), 500*time.Millisecond, func() bool { funcsStatus, err := mgr.FunctionsStatus(nil) suite.Require().Nil(err) for _, fn := range funcsStatus.Functions { if fn.Name == fnName { if fn.Status != EventingFunctionStateDeployed { suite.T().Logf("FunctionsStatus reports function not deployed: %s", fn.Status) } return fn.Status == EventingFunctionStateDeployed } } suite.T().Fatalf("Function not found from FunctionsStatus") return false }) suite.Require().True(success, "FunctionsStatus never reported function deployed") err = mgr.PauseFunction(fnName, nil) suite.Require().Nil(err, err) actualFn, err = mgr.GetFunction(fnName, nil) suite.Require().Nil(err, err) suite.Assert().Equal(EventingFunctionProcessingStatusPaused, actualFn.Settings.ProcessingStatus) success = suite.tryUntil(time.Now().Add(60*time.Second), 500*time.Millisecond, func() bool { funcsStatus, err := mgr.FunctionsStatus(nil) suite.Require().Nil(err) for _, fn := range funcsStatus.Functions { if fn.Name == fnName { if fn.Status != EventingFunctionStatePaused { suite.T().Logf("FunctionsStatus reports function not paused: %s", fn.Status) } return fn.Status == EventingFunctionStatePaused } } suite.T().Fatalf("Function not found from FunctionsStatus") return false }) suite.Require().True(success, "FunctionsStatus never reported function paused") err = mgr.UndeployFunction(fnName, nil) suite.Require().Nil(err, err) success = suite.tryUntil(time.Now().Add(30*time.Second), 500*time.Millisecond, func() bool { funcsStatus, err := mgr.FunctionsStatus(nil) suite.Require().Nil(err) for _, fn := range funcsStatus.Functions { if fn.Name == fnName { if fn.Status != EventingFunctionStateUndeployed { suite.T().Logf("FunctionsStatus reports function not undeployed: %s", fn.Status) } return fn.Status == EventingFunctionStateUndeployed } } suite.T().Fatalf("Function not found from FunctionsStatus") return false }) suite.Require().True(success, "FunctionsStatus never reported function undeployed") err = mgr.DropFunction(fnName, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) mustCreateScope(scope string) { cmgr := globalBucket.Collections() err := cmgr.CreateScope(scope, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) dropScope(scope string) { cmgr := globalBucket.Collections() err := cmgr.DropScope(scope, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) mustCreateCollection(scope, collection string) { cmgr := globalBucket.Collections() err := cmgr.CreateCollection(CollectionSpec{ Name: collection, ScopeName: scope, }, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) dropCollection(scope, collection string) { cmgr := globalBucket.Collections() err := cmgr.DropCollection(CollectionSpec{ Name: collection, ScopeName: scope, }, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) mustWaitForEventingCollections(scopeName string, collections []string) { suite.mustWaitForCollections(scopeName, collections) } func (suite *IntegrationTestSuite) mustWaitForCollections(scopeName string, collections []string) { success := suite.tryUntil(time.Now().Add(5*time.Second), 500*time.Millisecond, func() bool { scopes, err := globalBucket.Collections().GetAllScopes(nil) if err != nil { suite.T().Fatalf("Failed to GetAllScopes %v", err) } var scope *ScopeSpec for _, s := range scopes { if s.Name == scopeName { scope = &s break } } if scope == nil { return false } expected := len(collections) var actual int for _, col := range scope.Collections { for _, n := range collections { if col.Name == n { actual++ } } } return expected == actual }) suite.Require().True(success, "Collections did not come online in time") } gocb-2.6.3/cluster_internal.go000066400000000000000000000077251441755043100163570ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "github.com/google/uuid" "time" ) // InternalCluster is used for internal functionality. // Internal: This should never be used and is not supported. type InternalCluster struct { cluster *Cluster } // Internal returns an InternalCluster. // Internal: This should never be used and is not supported. func (c *Cluster) Internal() *InternalCluster { return &InternalCluster{ cluster: c, } } // NodeMetadata contains information about a node in the cluster. // Internal: This should never be used and is not supported. type NodeMetadata struct { ClusterCompatibility int ClusterMembership string CouchAPIBase string Hostname string InterestingStats map[string]float64 MCDMemoryAllocated float64 MCDMemoryReserved float64 MemoryFree float64 MemoryTotal float64 OS string Ports map[string]int Status string Uptime int Version string ThisNode bool } type jsonClusterCfg struct { Nodes []jsonNodeMetadata `json:"nodes"` } type jsonNodeMetadata struct { ClusterCompatibility int `json:"clusterCompatibility"` ClusterMembership string `json:"clusterMembership"` CouchAPIBase string `json:"couchApiBase"` Hostname string `json:"hostname"` InterestingStats map[string]float64 `json:"interestingStats,omitempty"` MCDMemoryAllocated float64 `json:"mcdMemoryAllocated"` MCDMemoryReserved float64 `json:"mcdMemoryReserved"` MemoryFree float64 `json:"memoryFree"` MemoryTotal float64 `json:"memoryTotal"` OS string `json:"os"` Ports map[string]int `json:"ports"` Status string `json:"status"` Uptime int `json:"uptime,string"` Version string `json:"version"` ThisNode bool `json:"thisNode,omitempty"` } // GetNodesMetadataOptions is the set of options available to the GetNodesMetadata operation. // Internal: This should never be used and is not supported. type GetNodesMetadataOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetNodesMetadata returns a list of information about nodes in the cluster. func (ic *InternalCluster) GetNodesMetadata(opts *GetNodesMetadataOptions) ([]NodeMetadata, error) { if opts == nil { opts = &GetNodesMetadataOptions{} } path := "/pools/default" start := time.Now() defer ic.cluster.meter.ValueRecord(meterValueServiceManagement, "internal_get_nodes_metadata", start) span := createSpan(ic.cluster.tracer, opts.ParentSpan, "internal_get_nodes_metadata", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Path: path, Method: "GET", IsIdempotent: true, RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := ic.cluster.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { return nil, makeMgmtBadStatusError("failed to get nodes metadata", &req, resp) } var nodesData jsonClusterCfg jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&nodesData) if err != nil { return nil, err } nodes := make([]NodeMetadata, len(nodesData.Nodes)) for i, nodeData := range nodesData.Nodes { nodes[i] = NodeMetadata(nodeData) } return nodes, nil } gocb-2.6.3/cluster_internal_test.go000066400000000000000000000003621441755043100174040ustar00rootroot00000000000000package gocb func (suite *IntegrationTestSuite) TestInternalClusterGetNodesMetadata() { ic := globalCluster.Internal() nodes, err := ic.GetNodesMetadata(nil) suite.Require().Nil(err, err) suite.Assert().GreaterOrEqual(len(nodes), 1) } gocb-2.6.3/cluster_ping.go000066400000000000000000000050511441755043100154660ustar00rootroot00000000000000package gocb import ( "context" "time" "github.com/couchbase/gocbcore/v10" "github.com/google/uuid" ) // Ping will ping a list of services and verify they are active and // responding in an acceptable period of time. func (c *Cluster) Ping(opts *PingOptions) (*PingResult, error) { if opts == nil { opts = &PingOptions{} } startTime := time.Now() defer c.meter.ValueRecord(meterValueServiceKV, "ping", startTime) span := createSpan(c.tracer, opts.ParentSpan, "ping", "kv") defer span.End() provider, err := c.getDiagnosticsProvider() if err != nil { return nil, err } return ping(opts.Context, provider, opts, c.timeoutsConfig, span) } func ping(ctx context.Context, provider diagnosticsProvider, opts *PingOptions, timeouts TimeoutsConfig, parentSpan RequestSpan) (*PingResult, error) { services := opts.ServiceTypes gocbcoreServices := make([]gocbcore.ServiceType, len(services)) for i, svc := range services { gocbcoreServices[i] = gocbcore.ServiceType(svc) } coreopts := gocbcore.PingOptions{ ServiceTypes: gocbcoreServices, TraceContext: parentSpan.Context(), } now := time.Now() timeout := opts.Timeout if timeout == 0 { coreopts.KVDeadline = now.Add(timeouts.KVTimeout) coreopts.CapiDeadline = now.Add(timeouts.ViewTimeout) coreopts.N1QLDeadline = now.Add(timeouts.QueryTimeout) coreopts.CbasDeadline = now.Add(timeouts.AnalyticsTimeout) coreopts.FtsDeadline = now.Add(timeouts.SearchTimeout) coreopts.MgmtDeadline = now.Add(timeouts.ManagementTimeout) } else { coreopts.KVDeadline = now.Add(timeout) coreopts.CapiDeadline = now.Add(timeout) coreopts.N1QLDeadline = now.Add(timeout) coreopts.CbasDeadline = now.Add(timeout) coreopts.FtsDeadline = now.Add(timeout) coreopts.MgmtDeadline = now.Add(timeout) } id := opts.ReportID if id == "" { id = uuid.New().String() } result, err := provider.Ping(ctx, coreopts) if err != nil { return nil, err } reportSvcs := make(map[ServiceType][]EndpointPingReport) for svcType, svc := range result.Services { st := ServiceType(svcType) svcs := make([]EndpointPingReport, len(svc)) for i, rep := range svc { var errStr string if rep.Error != nil { errStr = rep.Error.Error() } svcs[i] = EndpointPingReport{ ID: rep.ID, Remote: rep.Endpoint, State: PingState(rep.State), Error: errStr, Namespace: rep.Scope, Latency: rep.Latency, } } reportSvcs[st] = svcs } return &PingResult{ ID: id, sdk: Identifier() + " " + "gocbcore/" + gocbcore.Version(), Services: reportSvcs, }, nil } gocb-2.6.3/cluster_ping_test.go000066400000000000000000000147731441755043100165400ustar00rootroot00000000000000package gocb import ( "errors" "time" "github.com/stretchr/testify/mock" gocbcore "github.com/couchbase/gocbcore/v10" ) func (suite *IntegrationTestSuite) TestClusterPingAll() { suite.skipIfUnsupported(PingFeature) report, err := globalCluster.Ping(nil) suite.Require().Nil(err) suite.Assert().NotEmpty(report.ID) numServices := 3 if globalCluster.SupportsFeature(PingAnalyticsFeature) { numServices++ } suite.Assert().Len(report.Services, numServices) for serviceType, services := range report.Services { for _, service := range services { switch serviceType { case ServiceTypeQuery: suite.Assert().NotEmpty(service.Remote) suite.Assert().Equal(PingStateOk, service.State) suite.Assert().NotZero(int64(service.Latency)) case ServiceTypeSearch: suite.Assert().NotEmpty(service.Remote) suite.Assert().Equal(PingStateOk, service.State) suite.Assert().NotZero(int64(service.Latency)) case ServiceTypeManagement: suite.Assert().NotEmpty(service.Remote) suite.Assert().Equal(PingStateOk, service.State) suite.Assert().NotZero(int64(service.Latency)) case ServiceTypeAnalytics: if globalCluster.SupportsFeature(PingAnalyticsFeature) { suite.Assert().NotEmpty(service.Remote) suite.Assert().Equal(PingStateOk, service.State) suite.Assert().NotZero(int64(service.Latency)) } default: suite.T().Fatalf("Unexpected service type: %d", serviceType) } } } } func (suite *UnitTestSuite) TestClusterPingAll() { expectedResults := map[gocbcore.ServiceType][]gocbcore.EndpointPingResult{ gocbcore.N1qlService: { { Endpoint: "server1", Latency: 50 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, { Endpoint: "server2", Latency: 34 * time.Millisecond, Error: errors.New("something"), Scope: "default", State: gocbcore.PingStateError, }, }, gocbcore.CbasService: { { Endpoint: "server1", Latency: 50 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, }, gocbcore.FtsService: { { Endpoint: "server3", Latency: 20 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, }, } pingResult := &gocbcore.PingResult{ ConfigRev: 64, Services: expectedResults, } pingProvider := new(mockDiagnosticsProvider) pingProvider. On("Ping", nil, mock.AnythingOfType("gocbcore.PingOptions")). Run(func(args mock.Arguments) { opts := args.Get(1).(gocbcore.PingOptions) if len(opts.ServiceTypes) != 0 { suite.T().Errorf("Expected service types to be len 0 but was %v", opts.ServiceTypes) } }). Return(pingResult, nil) cli := new(mockConnectionManager) cli.On("getDiagnosticsProvider", "").Return(pingProvider, nil) c := &Cluster{ timeoutsConfig: TimeoutsConfig{ KVTimeout: 1000 * time.Second, AnalyticsTimeout: 1000 * time.Second, QueryTimeout: 1000 * time.Second, SearchTimeout: 1000 * time.Second, }, connectionManager: cli, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } report, err := c.Ping(nil) if err != nil { suite.T().Fatalf("Expected ping to not return error but was %v", err) } if report.ID == "" { suite.T().Fatalf("Report ID was empty") } if len(report.Services) != 3 { suite.T().Fatalf("Expected services length to be 3 but was %d", len(report.Services)) } for serviceType, services := range report.Services { expectedServices, ok := expectedResults[gocbcore.ServiceType(serviceType)] if !ok { suite.T().Errorf("Unexpected service type in result: %v", serviceType) continue } for i, service := range services { expectedService := expectedServices[i] suite.Assert().Equal(expectedService.Latency, service.Latency) suite.Assert().Equal(expectedService.Scope, service.Namespace) if expectedService.Error == nil { suite.Assert().Empty(service.Error) } else { suite.Assert().Equal(expectedService.Error.Error(), service.Error) } suite.Assert().Equal(PingState(expectedService.State), service.State) suite.Assert().Equal(expectedService.Endpoint, service.Remote) suite.Assert().Equal(expectedService.ID, service.ID) } } } func (suite *UnitTestSuite) TestClusterPingOne() { expectedResults := map[gocbcore.ServiceType][]gocbcore.EndpointPingResult{ gocbcore.N1qlService: { { Endpoint: "server1", Latency: 50 * time.Millisecond, Scope: "default", State: gocbcore.PingStateOK, }, { Endpoint: "server2", Latency: 34 * time.Millisecond, Error: errors.New("something"), Scope: "default", State: gocbcore.PingStateError, }, }, } pingResult := &gocbcore.PingResult{ ConfigRev: 64, Services: expectedResults, } pingProvider := new(mockDiagnosticsProvider) pingProvider. On("Ping", nil, mock.AnythingOfType("gocbcore.PingOptions")). Run(func(args mock.Arguments) { opts := args.Get(1).(gocbcore.PingOptions) if len(opts.ServiceTypes) != 1 { suite.T().Errorf("Expected service types to be len 1 but was %v", opts.ServiceTypes) } }). Return(pingResult, nil) cli := new(mockConnectionManager) cli.On("getDiagnosticsProvider", "").Return(pingProvider, nil) c := &Cluster{ timeoutsConfig: TimeoutsConfig{ KVTimeout: 1000 * time.Second, AnalyticsTimeout: 1000 * time.Second, QueryTimeout: 1000 * time.Second, SearchTimeout: 1000 * time.Second, }, connectionManager: cli, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } reportID := "myreportid" report, err := c.Ping(&PingOptions{ ServiceTypes: []ServiceType{ServiceTypeQuery}, ReportID: reportID, }) suite.Require().Nil(err) suite.Assert().Equal(reportID, report.ID) suite.Assert().Len(report.Services, 1) suite.Assert().Contains(report.Services, ServiceTypeQuery) queryServices := report.Services[ServiceTypeQuery] suite.Assert().Len(queryServices, 2) for i, service := range queryServices { expectedService := expectedResults[gocbcore.N1qlService][i] suite.Assert().Equal(expectedService.Latency, service.Latency) suite.Assert().Equal(expectedService.Scope, service.Namespace) if expectedService.Error == nil { suite.Assert().Empty(service.Error) } else { suite.Assert().Equal(expectedService.Error.Error(), service.Error) } suite.Assert().Equal(PingState(expectedService.State), service.State) suite.Assert().Equal(expectedService.Endpoint, service.Remote) suite.Assert().Equal(expectedService.ID, service.ID) } } gocb-2.6.3/cluster_query.go000066400000000000000000000272641441755043100157100ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) type jsonQueryMetrics struct { ElapsedTime string `json:"elapsedTime"` ExecutionTime string `json:"executionTime"` ResultCount uint64 `json:"resultCount"` ResultSize uint64 `json:"resultSize"` MutationCount uint64 `json:"mutationCount,omitempty"` SortCount uint64 `json:"sortCount,omitempty"` ErrorCount uint64 `json:"errorCount,omitempty"` WarningCount uint64 `json:"warningCount,omitempty"` } type jsonQueryWarning struct { Code uint32 `json:"code"` Message string `json:"msg"` } type jsonQueryResponse struct { RequestID string `json:"requestID"` ClientContextID string `json:"clientContextID"` Status QueryStatus `json:"status"` Warnings []jsonQueryWarning `json:"warnings"` Metrics *jsonQueryMetrics `json:"metrics,omitempty"` Profile interface{} `json:"profile"` Signature interface{} `json:"signature"` Prepared string `json:"prepared"` } // QueryMetrics encapsulates various metrics gathered during a queries execution. type QueryMetrics struct { ElapsedTime time.Duration ExecutionTime time.Duration ResultCount uint64 ResultSize uint64 MutationCount uint64 SortCount uint64 ErrorCount uint64 WarningCount uint64 } func (metrics *QueryMetrics) fromData(data *jsonQueryMetrics) error { elapsedTime, err := time.ParseDuration(data.ElapsedTime) if err != nil { logDebugf("Failed to parse query metrics elapsed time: %s", err) } executionTime, err := time.ParseDuration(data.ExecutionTime) if err != nil { logDebugf("Failed to parse query metrics execution time: %s", err) } metrics.ElapsedTime = elapsedTime metrics.ExecutionTime = executionTime metrics.ResultCount = data.ResultCount metrics.ResultSize = data.ResultSize metrics.MutationCount = data.MutationCount metrics.SortCount = data.SortCount metrics.ErrorCount = data.ErrorCount metrics.WarningCount = data.WarningCount return nil } // QueryWarning encapsulates any warnings returned by a query. type QueryWarning struct { Code uint32 Message string } func (warning *QueryWarning) fromData(data jsonQueryWarning) error { warning.Code = data.Code warning.Message = data.Message return nil } // QueryMetaData provides access to the meta-data properties of a query result. type QueryMetaData struct { RequestID string ClientContextID string Status QueryStatus Metrics QueryMetrics Signature interface{} Warnings []QueryWarning Profile interface{} preparedName string } func (meta *QueryMetaData) fromData(data jsonQueryResponse) error { metrics := QueryMetrics{} if data.Metrics != nil { if err := metrics.fromData(data.Metrics); err != nil { return err } } warnings := make([]QueryWarning, len(data.Warnings)) for wIdx, jsonWarning := range data.Warnings { err := warnings[wIdx].fromData(jsonWarning) if err != nil { return err } } meta.RequestID = data.RequestID meta.ClientContextID = data.ClientContextID meta.Status = data.Status meta.Metrics = metrics meta.Signature = data.Signature meta.Warnings = warnings meta.Profile = data.Profile meta.preparedName = data.Prepared return nil } // QueryResultRaw provides raw access to query data. // VOLATILE: This API is subject to change at any time. type QueryResultRaw struct { reader queryRowReader transactionID string nextRowBytes []byte } // NextBytes returns the next row as bytes. func (qrr *QueryResultRaw) NextBytes() []byte { rowBytes := qrr.nextRowBytes qrr.nextRowBytes = qrr.reader.NextRow() return rowBytes } // Err returns any errors that have occurred on the stream func (qrr *QueryResultRaw) Err() error { err := qrr.reader.Err() if err != nil { err = maybeEnhanceQueryError(err) if qrr.transactionID != "" { return singleQueryErrToTransactionError(err, qrr.transactionID) } return err } return nil } // Close marks the results as closed, returning any errors that occurred during reading the results. func (qrr *QueryResultRaw) Close() error { err := qrr.reader.Close() if err != nil { err = maybeEnhanceQueryError(err) if qrr.transactionID != "" { return singleQueryErrToTransactionError(err, qrr.transactionID) } return err } return nil } // MetaData returns any meta-data that was available from this query as bytes. func (qrr *QueryResultRaw) MetaData() ([]byte, error) { return qrr.reader.MetaData() } // QueryResult allows access to the results of a query. type QueryResult struct { reader queryRowReader transactionID string nextRowBytes []byte rowBytes []byte endpoint string } func newQueryResult(reader queryRowReader) *QueryResult { return &QueryResult{ reader: reader, endpoint: reader.Endpoint(), nextRowBytes: reader.NextRow(), } } // Raw returns a QueryResultRaw which can be used to access the raw byte data from search queries. // Calling this function invalidates the underlying QueryResult which will no longer be able to be used. // VOLATILE: This API is subject to change at any time. func (r *QueryResult) Raw() *QueryResultRaw { vr := &QueryResultRaw{ reader: r.reader, transactionID: r.transactionID, nextRowBytes: r.nextRowBytes, } r.reader = nil r.transactionID = "" r.nextRowBytes = nil return vr } func (r *QueryResult) peekNext() []byte { return r.nextRowBytes } // Next assigns the next result from the results into the value pointer, returning whether the read was successful. func (r *QueryResult) Next() bool { if r.reader == nil { return false } if len(r.nextRowBytes) == 0 { return false } r.rowBytes = r.nextRowBytes r.nextRowBytes = r.reader.NextRow() return true } // Row returns the contents of the current row func (r *QueryResult) Row(valuePtr interface{}) error { if r.reader == nil { return r.Err() } if r.rowBytes == nil { return ErrNoResult } if bytesPtr, ok := valuePtr.(*json.RawMessage); ok { *bytesPtr = r.rowBytes return nil } return json.Unmarshal(r.rowBytes, valuePtr) } // Err returns any errors that have occurred on the stream func (r *QueryResult) Err() error { if r.reader == nil { return errors.New("result object is no longer valid") } err := r.reader.Err() if err != nil { err = maybeEnhanceQueryError(err) if r.transactionID != "" { return singleQueryErrToTransactionError(err, r.transactionID) } return err } return nil } // Close marks the results as closed, returning any errors that occurred during reading the results. func (r *QueryResult) Close() error { if r.reader == nil { return r.Err() } err := r.reader.Close() if err != nil { err = maybeEnhanceQueryError(err) if r.transactionID != "" { return singleQueryErrToTransactionError(err, r.transactionID) } return err } return nil } // One assigns the first value from the results into the value pointer. // It will close the results but not before iterating through all remaining // results, as such this should only be used for very small resultsets - ideally // of, at most, length 1. func (r *QueryResult) One(valuePtr interface{}) error { if r.reader == nil { return r.Err() } // Read the bytes from the first row valueBytes := r.nextRowBytes if valueBytes == nil { return ErrNoResult } // Skip through the remaining rows for r.reader.NextRow() != nil { // do nothing with the row } r.nextRowBytes = nil return json.Unmarshal(valueBytes, valuePtr) } // MetaData returns any meta-data that was available from this query. Note that // the meta-data will only be available once the object has been closed (either // implicitly or explicitly). func (r *QueryResult) MetaData() (*QueryMetaData, error) { if r.reader == nil { return nil, r.Err() } metaDataBytes, err := r.reader.MetaData() if err != nil { return nil, err } var jsonResp jsonQueryResponse err = json.Unmarshal(metaDataBytes, &jsonResp) if err != nil { return nil, err } var metaData QueryMetaData err = metaData.fromData(jsonResp) if err != nil { return nil, err } return &metaData, nil } // QueryResultInternal provides access to internal only functionality. // Internal: This should never be used and is not supported. type QueryResultInternal struct { endpoint string } // Internal provides access to internal only functionality. // Internal: This should never be used and is not supported. func (r *QueryResult) Internal() *QueryResultInternal { return &QueryResultInternal{ endpoint: r.endpoint, } } // Endpoint returns the endpoint that this query was sent to. func (r *QueryResultInternal) Endpoint() string { return r.endpoint } type queryRowReader interface { NextRow() []byte Err() error MetaData() ([]byte, error) Close() error PreparedName() (string, error) Endpoint() string } // Query executes the query statement on the server. func (c *Cluster) Query(statement string, opts *QueryOptions) (*QueryResult, error) { if opts == nil { opts = &QueryOptions{} } if opts.AsTransaction != nil { return c.Transactions().singleQuery(statement, nil, *opts) } start := time.Now() defer c.meter.ValueRecord(meterValueServiceQuery, "query", start) span := createSpan(c.tracer, opts.ParentSpan, "query", "query") span.SetAttribute("db.statement", statement) defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = c.timeoutsConfig.QueryTimeout } deadline := time.Now().Add(timeout) retryStrategy := c.retryStrategyWrapper if opts.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(opts.RetryStrategy) } queryOpts, err := opts.toMap() if err != nil { return nil, QueryError{ InnerError: wrapError(err, "failed to generate query options"), Statement: statement, ClientContextID: opts.ClientContextID, } } queryOpts["statement"] = statement provider, err := c.getQueryProvider() if err != nil { return nil, QueryError{ InnerError: wrapError(err, "failed to get query provider"), Statement: statement, ClientContextID: maybeGetQueryOption(queryOpts, "client_context_id"), } } return execN1qlQuery( opts.Context, span, queryOpts, deadline, retryStrategy, opts.Adhoc, provider, c.tracer, opts.Internal.User, opts.Internal.Endpoint, ) } func maybeGetQueryOption(options map[string]interface{}, name string) string { if value, ok := options[name].(string); ok { return value } return "" } func execN1qlQuery( ctx context.Context, span RequestSpan, options map[string]interface{}, deadline time.Time, retryStrategy *retryStrategyWrapper, adHoc bool, provider queryProvider, tracer RequestTracer, user, endpoint string, ) (*QueryResult, error) { eSpan := tracer.RequestSpan(span.Context(), "request_encoding") eSpan.SetAttribute("db.system", "couchbase") reqBytes, err := json.Marshal(options) eSpan.End() if err != nil { return nil, QueryError{ InnerError: wrapError(err, "failed to marshall query body"), Statement: maybeGetQueryOption(options, "statement"), ClientContextID: maybeGetQueryOption(options, "client_context_id"), } } var res queryRowReader var qErr error if adHoc { res, qErr = provider.N1QLQuery(ctx, gocbcore.N1QLQueryOptions{ Payload: reqBytes, RetryStrategy: retryStrategy, Deadline: deadline, TraceContext: span.Context(), User: user, Endpoint: endpoint, }) } else { res, qErr = provider.PreparedN1QLQuery(ctx, gocbcore.N1QLQueryOptions{ Payload: reqBytes, RetryStrategy: retryStrategy, Deadline: deadline, TraceContext: span.Context(), User: user, Endpoint: endpoint, }) } if qErr != nil { return nil, maybeEnhanceQueryError(qErr) } return newQueryResult(res), nil } gocb-2.6.3/cluster_query_test.go000066400000000000000000000713201441755043100167370ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "github.com/google/uuid" "time" "github.com/couchbase/gocbcore/v10" "github.com/stretchr/testify/mock" ) type testQueryDataset struct { Results []testBreweryDocument jsonQueryResponse } type queryIface interface { Query(string, *QueryOptions) (*QueryResult, error) } func (suite *IntegrationTestSuite) TestQuery() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) n := suite.setupClusterQuery() suite.Run("TestClusterQuery", func() { suite.runClusterQueryPositionalTest(n, true) suite.runClusterQueryNamedTest(n, true) }) suite.Run("TestClusterQueryNoMetrics", func() { suite.runClusterQueryPositionalTest(n, false) suite.runClusterQueryNamedTest(n, false) }) suite.Run("TestClusterPreparedQuery", func() { suite.runClusterPreparedQueryPositionalTest(n) suite.runClusterPreparedQueryNamedTest(n) }) } func (suite *IntegrationTestSuite) runPreparedQueryTest(n int, query, bucket, scope string, queryFn queryIface, params interface{}) { suite.skipIfUnsupported(ClusterLevelQueryFeature) deadline := time.Now().Add(60 * time.Second) for { globalTracer.Reset() globalMeter.Reset() contextID := "contextID" opts := &QueryOptions{ Timeout: 5 * time.Second, ClientContextID: contextID, } switch p := params.(type) { case []interface{}: opts.PositionalParameters = p case map[string]interface{}: opts.NamedParameters = p } result, err := queryFn.Query(query, opts) suite.Require().Nil(err, "Failed to execute query %v", err) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(1, len(nilParents)) numDispatchSpans := 1 if !globalCluster.SupportsFeature(EnhancedPreparedStatementsFeature) { // Old style prepared statements means 2 requests. numDispatchSpans = 2 } suite.AssertHTTPOpSpan(nilParents[0], "query", HTTPOpSpanExpectations{ bucket: bucket, scope: scope, statement: query, numDispatchSpans: numDispatchSpans, atLeastNumDispatchSpans: false, hasEncoding: true, service: "query", dispatchOperationID: contextID, }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "query", "query"), 1, false) var samples []interface{} for result.Next() { var sample interface{} err := result.Row(&sample) suite.Require().Nil(err, "Failed to get value from row %v", err) samples = append(samples, sample) } err = result.Err() suite.Require().Nil(err, "Result had error %v", err) metadata, err := result.MetaData() suite.Require().Nil(err, "Metadata had error: %v", err) suite.Assert().NotEmpty(metadata.RequestID) if n == len(samples) { return } sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for indexing") return } } } func (suite *IntegrationTestSuite) runClusterPreparedQueryPositionalTest(n int) { suite.skipIfUnsupported(ClusterLevelQueryFeature) query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=? LIMIT %d;", globalBucket.Name(), globalBucket.Name(), n) suite.runPreparedQueryTest(n, query, "", "", globalCluster, []interface{}{"query"}) } func (suite *IntegrationTestSuite) runClusterPreparedQueryNamedTest(n int) { suite.skipIfUnsupported(ClusterLevelQueryFeature) query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=$service LIMIT %d;", globalBucket.Name(), globalBucket.Name(), n) suite.runPreparedQueryTest(n, query, "", "", globalCluster, map[string]interface{}{"service": "query"}) } func (suite *IntegrationTestSuite) TestClusterQueryImprovedErrorsDocNotFound() { suite.skipIfUnsupported(QueryImprovedErrorsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) suite.setupClusterQuery() query := fmt.Sprintf("INSERT INTO `%s` (KEY, VALUE) VALUES (\"%s\", \"test\")", globalBucket.Name(), uuid.New().String()) success := suite.tryUntil(time.Now().Add(60*time.Second), 500*time.Millisecond, func() bool { res, err := globalCluster.Query(query, nil) if err != nil && !errors.Is(err, ErrIndexNotFound) { suite.T().Logf("Error performing query: %v", err) return false } for res.Next() { } err = res.Err() if err != nil && !errors.Is(err, ErrIndexNotFound) { suite.T().Logf("Error performing query: %v", err) return false } return true }) suite.Require().True(success, "Timed out waiting for query to succeed") res, err := globalCluster.Query(query, nil) suite.Require().Nil(err, err) for res.Next() { } err = res.Err() suite.Require().ErrorIs(err, ErrDocumentExists) var qErr *QueryError suite.Require().ErrorAs(err, &qErr) suite.Require().Len(qErr.Errors, 1) suite.Assert().Equal(uint32(12009), qErr.Errors[0].Code) suite.Assert().Equal(float64(17012), qErr.Errors[0].Reason["code"]) } func (suite *IntegrationTestSuite) runQueryTest(n int, query, bucket, scope string, queryFn queryIface, withMetrics bool, params interface{}) { suite.skipIfUnsupported(ClusterLevelQueryFeature) deadline := time.Now().Add(60 * time.Second) for { globalTracer.Reset() globalMeter.Reset() contextID := "contextID" opts := &QueryOptions{ Timeout: 5 * time.Second, Adhoc: true, Metrics: withMetrics, ClientContextID: contextID, } switch p := params.(type) { case []interface{}: opts.PositionalParameters = p case map[string]interface{}: opts.NamedParameters = p } result, err := queryFn.Query(query, opts) suite.Require().Nil(err, "Failed to execute query %v", err) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(1, len(nilParents)) suite.AssertHTTPOpSpan(nilParents[0], "query", HTTPOpSpanExpectations{ bucket: bucket, scope: scope, statement: query, numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "query", dispatchOperationID: contextID, }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "query", "query"), 1, false) var samples []interface{} for result.Next() { var sample interface{} err := result.Row(&sample) suite.Require().Nil(err, "Failed to get value from row %v", err) samples = append(samples, sample) } err = result.Err() suite.Require().Nil(err, "Result had error %v", err) metadata, err := result.MetaData() suite.Require().Nil(err, "Metadata had error: %v", err) suite.Assert().NotEmpty(metadata.RequestID) if withMetrics { suite.Assert().NotZero(metadata.Metrics.ElapsedTime) suite.Assert().NotZero(metadata.Metrics.ExecutionTime) } if n == len(samples) { if withMetrics { suite.Assert().NotZero(metadata.Metrics.ResultCount) suite.Assert().NotZero(metadata.Metrics.ResultSize) } return } sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for indexing") return } } } func (suite *IntegrationTestSuite) runClusterQueryPositionalTest(n int, withMetrics bool) { suite.skipIfUnsupported(ClusterLevelQueryFeature) query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=? LIMIT %d;", globalBucket.Name(), globalBucket.Name(), n) suite.runQueryTest(n, query, "", "", globalCluster, withMetrics, []interface{}{"query"}) } func (suite *IntegrationTestSuite) runClusterQueryNamedTest(n int, withMetrics bool) { suite.skipIfUnsupported(ClusterLevelQueryFeature) query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=$service LIMIT %d;", globalBucket.Name(), globalBucket.Name(), n) suite.runQueryTest(n, query, "", "", globalCluster, withMetrics, map[string]interface{}{"service": "query"}) } func (suite *IntegrationTestSuite) setupClusterQuery() int { suite.skipIfUnsupported(ClusterLevelQueryFeature) n, err := suite.createBreweryDataset("beer_sample_brewery_five", "query", "", "") suite.Require().Nil(err, "Failed to create dataset %v", err) mgr := globalCluster.QueryIndexes() err = mgr.CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, Timeout: 30 * time.Second, }) suite.Require().Nil(err, "Failed to create index %v", err) return n } func (suite *IntegrationTestSuite) TestClusterQueryContext() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) ctx, cancel := context.WithCancel(context.Background()) cancel() res, err := globalCluster.Query("SELECT 1=1", &QueryOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) defer cancel() res, err = globalCluster.Query("SELECT 1=1", &QueryOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) } func (suite *IntegrationTestSuite) TestClusterQueryTransaction() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) mgr := globalCluster.QueryIndexes() err := mgr.CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) suite.Eventually(func() bool { res, err := globalCluster.Query(fmt.Sprintf("SELECT 1 FROM %s", globalBucket.Name()), &QueryOptions{ Adhoc: true, }) if err != nil { return false } for res.Next() { } err = res.Err() return err == nil }, 30*time.Second, 500*time.Millisecond) res, err := globalCluster.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {})", globalBucket.Name(), uuid.New().String()), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) suite.Require().Nil(err, err) for res.Next() { } err = res.Err() suite.Require().Nil(err, err) meta, err := res.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(uint64(1), meta.Metrics.MutationCount) } func (suite *IntegrationTestSuite) TestClusterQueryTransactionDoubleInsert() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) mgr := globalCluster.QueryIndexes() err := mgr.CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) suite.Eventually(func() bool { res, err := globalCluster.Query(fmt.Sprintf("SELECT 1 FROM %s", globalBucket.Name()), &QueryOptions{ Adhoc: true, }) if err != nil { return false } for res.Next() { } err = res.Err() return err == nil }, 30*time.Second, 500*time.Millisecond) docID := uuid.New().String() res, err := globalCluster.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {})", globalBucket.Name(), docID), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) suite.Require().Nil(err, err) for res.Next() { } err = res.Err() suite.Require().Nil(err, err) meta, err := res.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(uint64(1), meta.Metrics.MutationCount) _, err = globalCluster.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {})", globalBucket.Name(), docID), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) suite.Require().Error(err, err) if globalCluster.SupportsFeature(TransactionsSingleQueryExistsErrorFeature) { var tErr *TransactionFailedError if errors.As(err, &tErr) { suite.T().Logf("Error should have not have been TransactionFailed but was: %v", err) suite.T().Fail() } suite.Require().ErrorIs(err, ErrDocumentExists) } else { var tErr *TransactionFailedError suite.Assert().ErrorAs(err, &tErr) } } func (suite *IntegrationTestSuite) TestClusterQueryTransactionOne() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) mgr := globalCluster.QueryIndexes() err := mgr.CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) suite.Eventually(func() bool { res, err := globalCluster.Query(fmt.Sprintf("SELECT 1 FROM %s", globalBucket.Name()), &QueryOptions{ Adhoc: true, }) if err != nil { return false } for res.Next() { } err = res.Err() return err == nil }, 30*time.Second, 500*time.Millisecond) docID := uuid.New().String() res, err := globalCluster.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {}) RETURNING meta().id", globalBucket.Name(), docID), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) suite.Require().Nil(err, err) var something interface{} err = res.One(&something) suite.Require().NoError(err, err) suite.Assert().Equal(map[string]interface{}{"id": docID}, something) err = res.Err() suite.Require().Nil(err, err) meta, err := res.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(uint64(1), meta.Metrics.MutationCount) } // We have to manually mock this because testify won't let return something which can iterate. type mockQueryRowReader struct { Dataset []testBreweryDocument mockQueryRowReaderBase } type mockQueryRowReaderBase struct { Meta []byte MetaErr error CloseErr error RowsErr error PName string Suite *UnitTestSuite idx int } func (arr *mockQueryRowReader) NextRow() []byte { if arr.idx == len(arr.Dataset) { return nil } idx := arr.idx arr.idx++ return arr.Suite.mustConvertToBytes(arr.Dataset[idx]) } func (arr *mockQueryRowReaderBase) MetaData() ([]byte, error) { return arr.Meta, arr.MetaErr } func (arr *mockQueryRowReaderBase) Close() error { return arr.CloseErr } func (arr *mockQueryRowReaderBase) Err() error { return arr.RowsErr } func (arr *mockQueryRowReaderBase) PreparedName() (string, error) { return arr.PName, nil } func (arr *mockQueryRowReaderBase) Endpoint() string { return "" } func (suite *UnitTestSuite) newMockQueryProvider(prepared bool, reader queryRowReader) (*mockQueryProvider, *mock.Call) { queryProvider := new(mockQueryProvider) methodName := "N1QLQuery" if prepared { methodName = "PreparedN1QLQuery" } call := queryProvider. On(methodName, nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Return(reader, nil). Once() return queryProvider, call } func (suite *UnitTestSuite) queryCluster(prepared bool, reader queryRowReader, runFn func(args mock.Arguments)) *Cluster { queryProvider, call := suite.newMockQueryProvider(prepared, reader) if runFn != nil { call.Run(runFn) } cli := new(mockConnectionManager) cli.On("getQueryProvider").Return(queryProvider, nil) cluster := suite.newCluster(cli) return cluster } func (suite *UnitTestSuite) assertQueryBeerResult(dataset testQueryDataset, result *QueryResult) { var breweries []testBreweryDocument for result.Next() { var doc testBreweryDocument err := result.Row(&doc) suite.Require().Nil(err, err) breweries = append(breweries, doc) } suite.Assert().Len(breweries, len(dataset.Results)) err := result.Err() suite.Require().Nil(err, err) metadata, err := result.MetaData() suite.Require().Nil(err, err) var aMeta QueryMetaData err = aMeta.fromData(dataset.jsonQueryResponse) suite.Require().Nil(err, err) suite.Assert().Equal(&aMeta, metadata) } func (suite *UnitTestSuite) TestQueryAdhoc() { var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, PName: dataset.jsonQueryResponse.Prepared, }, } statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.queryCluster(false, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Contains(actualOptions, "client_context_id") suite.Assert().Equal(statement, actualOptions["statement"]) }) result, err := cluster.Query(statement, &QueryOptions{ Adhoc: true, }) suite.Require().Nil(err, err) suite.Require().NotNil(result) suite.assertQueryBeerResult(dataset, result) } func (suite *UnitTestSuite) TestQueryPrepared() { var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, PName: dataset.jsonQueryResponse.Prepared, }, } statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.queryCluster(true, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Contains(actualOptions, "client_context_id") }) result, err := cluster.Query(statement, nil) suite.Require().Nil(err, err) suite.Require().NotNil(result) suite.assertQueryBeerResult(dataset, result) } func (suite *UnitTestSuite) TestQueryResultsOne() { var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, }, } result := newQueryResult(reader) var doc testBreweryDocument err = result.One(&doc) suite.Require().Nil(err, err) suite.Assert().Equal(dataset.Results[0], doc) // Test that One iterated all rows. var count int for result.Next() { count++ } suite.Assert().Zero(count) suite.Assert().Nil(result.reader.NextRow()) err = result.Err() suite.Require().Nil(err, err) metadata, err := result.MetaData() suite.Require().Nil(err, err) var aMeta QueryMetaData err = aMeta.fromData(dataset.jsonQueryResponse) suite.Require().Nil(err, err) suite.Assert().Equal(&aMeta, metadata) } func (suite *UnitTestSuite) TestQueryResultsErr() { reader := &mockQueryRowReader{ mockQueryRowReaderBase: mockQueryRowReaderBase{ RowsErr: errors.New("some error"), Suite: suite, }, } result := newQueryResult(reader) err := result.Err() suite.Require().NotNil(err, err) } func (suite *UnitTestSuite) TestQueryResultsCloseErr() { retErr := errors.New("some error") reader := &mockQueryRowReader{ mockQueryRowReaderBase: mockQueryRowReaderBase{ CloseErr: retErr, Suite: suite, }, } result := newQueryResult(reader) err := result.Close() suite.Require().Equal(retErr, err) } func (suite *UnitTestSuite) TestQueryResultsOneErr() { retErr := errors.New("some error") var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ RowsErr: retErr, Suite: suite, }, } result := newQueryResult(reader) var doc testBreweryDocument err = result.One(&doc) suite.Require().NoError(err, err) err = result.Err() suite.Require().Equal(retErr, err) } func (suite *UnitTestSuite) TestQueryUntypedError() { retErr := errors.New("an error") queryProvider := new(mockQueryProvider) queryProvider. On("N1QLQuery", nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Return(nil, retErr) cli := new(mockConnectionManager) cli.On("getQueryProvider").Return(queryProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.Query("SELECT * FROM dataset", &QueryOptions{ Adhoc: true, }) suite.Require().Equal(retErr, err) suite.Require().Nil(result) } func (suite *UnitTestSuite) TestQueryGocbcoreError() { retErr := &gocbcore.N1QLError{ Endpoint: "http://localhost:8093", Statement: "SELECT * FROM dataset", ClientContextID: "context", Errors: []gocbcore.N1QLErrorDesc{{Code: 5000, Message: "Internal Error"}}, } queryProvider := new(mockQueryProvider) queryProvider. On("N1QLQuery", nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Return(nil, retErr) cli := new(mockConnectionManager) cli.On("getQueryProvider").Return(queryProvider, nil) cluster := suite.newCluster(cli) result, err := cluster.Query("SELECT * FROM dataset", &QueryOptions{ Adhoc: true, }) suite.Require().IsType(&QueryError{}, err) suite.Require().Equal(&QueryError{ Endpoint: "http://localhost:8093", Statement: "SELECT * FROM dataset", ClientContextID: "context", Errors: []QueryErrorDesc{{Code: 5000, Message: "Internal Error"}}, }, err) suite.Require().Nil(result) } func (suite *UnitTestSuite) TestQueryTimeoutOption() { reader := new(mockQueryRowReader) cluster := suite.newCluster(nil) statement := "SELECT * FROM dataset" queryProvider := new(mockQueryProvider) queryProvider. On("N1QLQuery", nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Run(func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(20*time.Second)) || opts.Deadline.After(now.Add(25*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } }). Return(reader, nil) cli := new(mockConnectionManager) cli.On("getQueryProvider").Return(queryProvider, nil) cluster.connectionManager = cli result, err := cluster.Query(statement, &QueryOptions{ Timeout: 25 * time.Second, Adhoc: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestQueryNamedParams() { reader := new(mockQueryRowReader) statement := "SELECT * FROM dataset" params := map[string]interface{}{ "num": 1, "imafish": "namedbarry", "$cilit": "bang", } cluster := suite.queryCluster(false, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(float64(1), actualOptions["$num"]) suite.Assert().Equal("namedbarry", actualOptions["$imafish"]) suite.Assert().Equal("bang", actualOptions["$cilit"]) }) result, err := cluster.Query(statement, &QueryOptions{ NamedParameters: params, Adhoc: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestQueryPositionalParams() { reader := new(mockQueryRowReader) statement := "SELECT * FROM dataset" params := []interface{}{float64(1), "imafish"} cluster := suite.queryCluster(false, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) if suite.Assert().Contains(actualOptions, "args") { suite.Require().Equal(params, actualOptions["args"]) } }) result, err := cluster.Query(statement, &QueryOptions{ PositionalParameters: params, Adhoc: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestQueryClientContextID() { reader := new(mockQueryRowReader) statement := "SELECT * FROM dataset" contextID := "62d29101-0c9f-400d-af2b-9bd44a557a7c" cluster := suite.queryCluster(false, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(contextID, actualOptions["client_context_id"]) }) result, err := cluster.Query(statement, &QueryOptions{ ClientContextID: contextID, Adhoc: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } func (suite *UnitTestSuite) TestQueryNoMetrics() { var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset_no_metrics", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, PName: dataset.jsonQueryResponse.Prepared, }, } statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.queryCluster(true, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Contains(actualOptions, "client_context_id") }) result, err := cluster.Query(statement, nil) suite.Require().Nil(err, err) suite.Require().NotNil(result) suite.assertQueryBeerResult(dataset, result) metadata, err := result.MetaData() suite.Require().Nil(err, err) suite.Assert().Zero(metadata.Metrics.ElapsedTime) suite.Assert().Zero(metadata.Metrics.ErrorCount) suite.Assert().Zero(metadata.Metrics.ExecutionTime) suite.Assert().Zero(metadata.Metrics.MutationCount) suite.Assert().Zero(metadata.Metrics.ResultCount) suite.Assert().Zero(metadata.Metrics.ResultSize) suite.Assert().Zero(metadata.Metrics.SortCount) suite.Assert().Zero(metadata.Metrics.WarningCount) } func (suite *UnitTestSuite) TestQueryRaw() { var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, PName: dataset.jsonQueryResponse.Prepared, }, } statement := "SELECT * FROM dataset" var cluster *Cluster cluster = suite.queryCluster(false, reader, func(args mock.Arguments) {}) result, err := cluster.Query(statement, &QueryOptions{ Adhoc: true, }) suite.Require().Nil(err, err) suite.Require().NotNil(result) raw := result.Raw() suite.Assert().False(result.Next()) suite.Assert().Error(result.One([]string{})) suite.Assert().Error(result.Err()) suite.Assert().Error(result.Close()) suite.Assert().Error(result.Row([]string{})) _, err = result.MetaData() suite.Assert().Error(err) var i int for b := raw.NextBytes(); b != nil; b = raw.NextBytes() { suite.Assert().Equal(suite.mustConvertToBytes(dataset.Results[i]), b) i++ } err = raw.Err() suite.Require().Nil(err, err) metadata, err := raw.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(reader.Meta, metadata) } func (suite *UnitTestSuite) TestQueryPreserveExpiry() { reader := new(mockQueryRowReader) statement := "UPDATE default AS d SET d.comment = \"xyz\";" cluster := suite.queryCluster(false, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Equal(true, actualOptions["preserve_expiry"]) }) result, err := cluster.Query(statement, &QueryOptions{ PreserveExpiry: true, Adhoc: true, }) suite.Require().Nil(err) suite.Require().NotNil(result) } gocb-2.6.3/cluster_queryindexes.go000066400000000000000000000272431441755043100172650ustar00rootroot00000000000000package gocb import ( "context" "fmt" "time" ) // QueryIndexManager provides methods for performing Couchbase query index management. type QueryIndexManager struct { base *baseQueryIndexManager } func (qm *QueryIndexManager) maybeAddScopeCollectionToBucket(bucketName, scope, collection string) string { if scope != "" && collection != "" { bucketName = fmt.Sprintf("`%s`.`%s`.`%s`", bucketName, scope, collection) } else if collection == "" && scope != "" { bucketName = fmt.Sprintf("`%s`.`%s`.`_default", bucketName, scope) } else if collection != "" && scope == "" { bucketName = fmt.Sprintf("`%s`.`_default`.`%s", bucketName, collection) } else { bucketName = "`" + bucketName + "`" } return bucketName } func (qm *QueryIndexManager) validateScopeCollection(scope, collection string) error { if scope == "" && collection != "" { return makeInvalidArgumentsError("if collection is set then scope must be set") } else if scope != "" && collection == "" { return makeInvalidArgumentsError("if scope is set then collection must be set") } return nil } // CreateQueryIndexOptions is the set of options available to the query indexes CreateIndex operation. type CreateQueryIndexOptions struct { IgnoreIfExists bool Deferred bool NumReplicas int Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreateIndex creates an index over the specified fields. // The SDK will automatically escape the provided index keys. For more advanced use cases like index keys using keywords // cluster.Query or scope.Query should be used with the query directly. func (qm *QueryIndexManager) CreateIndex(bucketName, indexName string, keys []string, opts *CreateQueryIndexOptions) error { if opts == nil { opts = &CreateQueryIndexOptions{} } if indexName == "" { return invalidArgumentsError{ message: "an invalid index name was specified", } } if len(keys) <= 0 { return invalidArgumentsError{ message: "you must specify at least one index-key to index", } } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.CreateIndex( opts.Context, opts.ParentSpan, qm.maybeAddScopeCollectionToBucket(bucketName, opts.ScopeName, opts.CollectionName), indexName, keys, createQueryIndexOptions{ IgnoreIfExists: opts.IgnoreIfExists, Deferred: opts.Deferred, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, NumReplicas: opts.NumReplicas, }, ) } // CreatePrimaryQueryIndexOptions is the set of options available to the query indexes CreatePrimaryIndex operation. type CreatePrimaryQueryIndexOptions struct { IgnoreIfExists bool Deferred bool CustomName string NumReplicas int Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // CreatePrimaryIndex creates a primary index. An empty customName uses the default naming. func (qm *QueryIndexManager) CreatePrimaryIndex(bucketName string, opts *CreatePrimaryQueryIndexOptions) error { if opts == nil { opts = &CreatePrimaryQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.CreateIndex( opts.Context, opts.ParentSpan, qm.maybeAddScopeCollectionToBucket(bucketName, opts.ScopeName, opts.CollectionName), opts.CustomName, nil, createQueryIndexOptions{ IgnoreIfExists: opts.IgnoreIfExists, Deferred: opts.Deferred, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, NumReplicas: opts.NumReplicas, }) } // DropQueryIndexOptions is the set of options available to the query indexes DropIndex operation. type DropQueryIndexOptions struct { IgnoreIfNotExists bool Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropIndex drops a specific index by name. func (qm *QueryIndexManager) DropIndex(bucketName, indexName string, opts *DropQueryIndexOptions) error { if opts == nil { opts = &DropQueryIndexOptions{} } if indexName == "" { return invalidArgumentsError{ message: "an invalid index name was specified", } } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.DropIndex( opts.Context, opts.ParentSpan, qm.maybeAddScopeCollectionToBucket(bucketName, opts.ScopeName, opts.CollectionName), indexName, dropQueryIndexOptions{ IgnoreIfNotExists: opts.IgnoreIfNotExists, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, UseCollectionsSyntax: opts.ScopeName != "" || opts.CollectionName != "", }) } // DropPrimaryQueryIndexOptions is the set of options available to the query indexes DropPrimaryIndex operation. type DropPrimaryQueryIndexOptions struct { IgnoreIfNotExists bool CustomName string Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropPrimaryIndex drops the primary index. Pass an empty customName for unnamed primary indexes. func (qm *QueryIndexManager) DropPrimaryIndex(bucketName string, opts *DropPrimaryQueryIndexOptions) error { if opts == nil { opts = &DropPrimaryQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.DropIndex( opts.Context, opts.ParentSpan, qm.maybeAddScopeCollectionToBucket(bucketName, opts.ScopeName, opts.CollectionName), opts.CustomName, dropQueryIndexOptions{ IgnoreIfNotExists: opts.IgnoreIfNotExists, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, UseCollectionsSyntax: opts.ScopeName != "" || opts.CollectionName != "", }) } // GetAllQueryIndexesOptions is the set of options available to the query indexes GetAllIndexes operation. type GetAllQueryIndexesOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } func (qm *QueryIndexManager) buildGetAllIndexesWhereClause( bucketName, scopeName, collectionName string, ) (string, map[string]interface{}) { bucketCond := "bucket_id = $bucketName" scopeCond := "(" + bucketCond + " AND scope_id = $scopeName)" collectionCond := "(" + scopeCond + " AND keyspace_id = $collectionName)" params := map[string]interface{}{ "bucketName": bucketName, } var where string if collectionName != "" { where = collectionCond params["scopeName"] = scopeName params["collectionName"] = collectionName } else if scopeName != "" { where = scopeCond params["scopeName"] = scopeName } else { where = bucketCond } if collectionName == "_default" || collectionName == "" { defaultColCond := "(bucket_id IS MISSING AND keyspace_id = $bucketName)" where = "(" + where + " OR " + defaultColCond + ")" } return where, params } // GetAllIndexes returns a list of all currently registered indexes. func (qm *QueryIndexManager) GetAllIndexes(bucketName string, opts *GetAllQueryIndexesOptions) ([]QueryIndex, error) { if opts == nil { opts = &GetAllQueryIndexesOptions{} } where, params := qm.buildGetAllIndexesWhereClause(bucketName, opts.ScopeName, opts.CollectionName) return qm.base.GetAllIndexes( opts.Context, opts.ParentSpan, where, params, getAllQueryIndexesOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // BuildDeferredQueryIndexOptions is the set of options available to the query indexes BuildDeferredIndexes operation. type BuildDeferredQueryIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // BuildDeferredIndexes builds all indexes which are currently in deferred state. // If no collection and scope names are specified in the options then *only* indexes created on the bucket directly // will be built. func (qm *QueryIndexManager) BuildDeferredIndexes(bucketName string, opts *BuildDeferredQueryIndexOptions) ([]string, error) { if opts == nil { opts = &BuildDeferredQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return nil, err } var where string params := make(map[string]interface{}) if opts.CollectionName == "" { where = "(keyspace_id = $bucketName AND bucket_id IS MISSING)" params["bucketName"] = bucketName } else { where = "bucket_id = $bucketName AND scope_id = $scopeName AND keyspace_id = $collectionName" params["bucketName"] = bucketName params["scopeName"] = opts.ScopeName params["collectionName"] = opts.CollectionName } return qm.base.BuildDeferredIndexes( qm.maybeAddScopeCollectionToBucket(bucketName, opts.ScopeName, opts.CollectionName), where, params, buildDeferredQueryIndexOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }, ) } // WatchQueryIndexOptions is the set of options available to the query indexes Watch operation. type WatchQueryIndexOptions struct { WatchPrimary bool RetryStrategy RetryStrategy ParentSpan RequestSpan ScopeName string CollectionName string // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // WatchIndexes waits for a set of indexes to come online. func (qm *QueryIndexManager) WatchIndexes(bucketName string, watchList []string, timeout time.Duration, opts *WatchQueryIndexOptions) error { if opts == nil { opts = &WatchQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } where, params := qm.buildGetAllIndexesWhereClause(bucketName, opts.ScopeName, opts.CollectionName) return qm.base.WatchIndexes( where, params, watchList, timeout, watchQueryIndexOptions{ WatchPrimary: opts.WatchPrimary, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }, ) } gocb-2.6.3/cluster_queryindexes_test.go000066400000000000000000000427061441755043100203250ustar00rootroot00000000000000package gocb import ( "errors" "time" "github.com/google/uuid" ) func (suite *IntegrationTestSuite) TestQueryIndexesCrud() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) bucketMgr := globalCluster.Buckets() bucketName := "testIndexes" err := bucketMgr.CreateBucket(CreateBucketSettings{ BucketSettings: BucketSettings{ Name: bucketName, RAMQuotaMB: 100, NumReplicas: 0, BucketType: CouchbaseBucketType, }, }, nil) suite.Require().Nil(err, err) defer bucketMgr.DropBucket(bucketName, nil) mgr := globalCluster.QueryIndexes() deadline := time.Now().Add(5 * time.Second) for { err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) if err == nil { break } suite.T().Logf("Failed to create primary index: %s", err) sleepDeadline := time.Now().Add(500 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for create index to succeed") return } } err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: false, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } err = mgr.CreateIndex(bucketName, "testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) err = mgr.CreateIndex(bucketName, "testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } // We create this first to give it a chance to be created by the time we need it. err = mgr.CreateIndex(bucketName, "testIndexDeferred", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, Deferred: true, }) suite.Require().Nil(err, err) indexNames, err := mgr.BuildDeferredIndexes(bucketName, nil) suite.Require().Nil(err, err) suite.Assert().Len(indexNames, 1) err = mgr.WatchIndexes(bucketName, []string{"testIndexDeferred"}, 60*time.Second, nil) suite.Require().Nil(err, err) indexes, err := mgr.GetAllIndexes(bucketName, nil) suite.Require().Nil(err, err) suite.Assert().Len(indexes, 3) var index QueryIndex for _, idx := range indexes { if idx.Name == "testIndex" { index = idx break } } suite.Assert().Equal("testIndex", index.Name) suite.Assert().False(index.IsPrimary) suite.Assert().Equal(QueryIndexTypeGsi, index.Type) suite.Assert().Equal("online", index.State) suite.Assert().Equal("testIndexes", index.Keyspace) suite.Assert().Equal("testIndexes", index.BucketName) suite.Assert().Equal("", index.ScopeName) suite.Assert().Equal("", index.CollectionName) suite.Assert().Equal("default", index.Namespace) if suite.Assert().Len(index.IndexKey, 1) { suite.Assert().Equal("`field`", index.IndexKey[0]) } suite.Assert().Empty(index.Condition) suite.Assert().Empty(index.Partition) err = mgr.DropIndex(bucketName, "testIndex", nil) suite.Require().Nil(err, err) err = mgr.DropIndex(bucketName, "testIndex", nil) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } err = mgr.DropPrimaryIndex(bucketName, nil) suite.Require().Nil(err, err) err = mgr.DropPrimaryIndex(bucketName, nil) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_bucket_create_bucket"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_create_primary_index"), 2, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_create_index"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_build_deferred_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_watch_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_get_all_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_drop_primary_index"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_drop_index"), 2, false) } func (suite *IntegrationTestSuite) TestQueryIndexesCrudCollections() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) suite.dropAllIndexes() bucketName := globalBucket.Name() scopeName := "testQueryIndexesScope" colName := "testQueryIndexesCollection" colmgr := globalBucket.Collections() err := colmgr.CreateScope(scopeName, nil) suite.Require().Nil(err, err) defer colmgr.DropScope(scopeName, nil) err = colmgr.CreateCollection(CollectionSpec{ ScopeName: scopeName, Name: colName, }, nil) suite.Require().Nil(err, err) mgr := globalCluster.QueryIndexes() deadline := time.Now().Add(60 * time.Second) for { err = mgr.CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, ScopeName: scopeName, CollectionName: colName, }) if err == nil { break } suite.T().Logf("Failed to create primary index: %s", err) sleepDeadline := time.Now().Add(500 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for create index to succeed") return } } err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ IgnoreIfExists: false, ScopeName: scopeName, CollectionName: colName, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } err = mgr.CreateIndex(bucketName, "testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: true, ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) err = mgr.CreateIndex(bucketName, "testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, ScopeName: scopeName, CollectionName: colName, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } // We create this first to give it a chance to be created by the time we need it. err = mgr.CreateIndex(bucketName, "testIndexDeferred", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, Deferred: true, ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) indexNames, err := mgr.BuildDeferredIndexes(bucketName, &BuildDeferredQueryIndexOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) suite.Assert().Len(indexNames, 1) err = mgr.WatchIndexes(bucketName, []string{"testIndexDeferred"}, 60*time.Second, &WatchQueryIndexOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) indexes, err := mgr.GetAllIndexes(bucketName, &GetAllQueryIndexesOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) suite.Assert().Len(indexes, 3) var index QueryIndex for _, idx := range indexes { if idx.Name == "testIndex" { index = idx break } } suite.Assert().Equal("testIndex", index.Name) suite.Assert().False(index.IsPrimary) suite.Assert().Equal(QueryIndexTypeGsi, index.Type) suite.Assert().Equal("online", index.State) suite.Assert().Equal(colName, index.Keyspace) suite.Assert().Equal("default", index.Namespace) suite.Assert().Equal(scopeName, index.ScopeName) suite.Assert().Equal(colName, index.CollectionName) suite.Assert().Equal(bucketName, index.BucketName) if suite.Assert().Len(index.IndexKey, 1) { suite.Assert().Equal("`field`", index.IndexKey[0]) } suite.Assert().Empty(index.Condition) suite.Assert().Empty(index.Partition) err = mgr.DropIndex(bucketName, "testIndex", &DropQueryIndexOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) err = mgr.DropIndex(bucketName, "testIndex", &DropQueryIndexOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } err = mgr.DropPrimaryIndex(bucketName, &DropPrimaryQueryIndexOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().Nil(err, err) err = mgr.DropPrimaryIndex(bucketName, &DropPrimaryQueryIndexOptions{ ScopeName: scopeName, CollectionName: colName, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_create_scope"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_create_collection"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_create_primary_index"), 2, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_create_index"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_build_deferred_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_watch_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_get_all_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_drop_primary_index"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_drop_index"), 2, false) } func (suite *IntegrationTestSuite) TestQueryIndexesBuildDeferredSameNamespaceNamesBucketOnly() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) suite.dropAllIndexes() bucketName := globalBucket.Name() collections := globalBucket.Collections() err := collections.CreateCollection(CollectionSpec{ ScopeName: "_default", Name: bucketName, }, nil) suite.Require().Nil(err, err) mgr := globalCluster.QueryIndexes() suite.tryUntil(time.Now().Add(5*time.Second), 100*time.Millisecond, func() bool { err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ Deferred: true, }) if err == nil || errors.Is(err, ErrIndexExists) { return true } suite.T().Logf("Unexpected error, retrying: %v", err) return false }) suite.tryUntil(time.Now().Add(5*time.Second), 100*time.Millisecond, func() bool { err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ Deferred: true, ScopeName: "_default", CollectionName: bucketName, }) if err == nil || errors.Is(err, ErrIndexExists) { return true } suite.T().Logf("Unexpected error, retrying: %v", err) return false }) names, err := mgr.BuildDeferredIndexes(bucketName, nil) suite.Require().Nil(err, err) suite.Assert().Equal([]string{"#primary"}, names) suite.Eventually(func() bool { indexes, err := mgr.GetAllIndexes(bucketName, nil) suite.Require().Nil(err, err) suite.Assert().Len(indexes, 2) for _, index := range indexes { if index.CollectionName == "" { if !(index.State == "building" || index.State == "online") { return false } } else { suite.Assert().Equal("deferred", index.State) } } return true }, 5*time.Second, 100*time.Millisecond) } func (suite *IntegrationTestSuite) TestQueryIndexesBuildDeferredSameNamespaceNamesCollectionOnly() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) suite.dropAllIndexes() bucketName := globalBucket.Name() collectionName := uuid.NewString() collections := globalBucket.Collections() err := collections.CreateCollection(CollectionSpec{ ScopeName: "_default", Name: collectionName, }, nil) suite.Require().Nil(err, err) mgr := globalCluster.QueryIndexes() suite.tryUntil(time.Now().Add(5*time.Second), 100*time.Millisecond, func() bool { err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ Deferred: true, }) if err == nil || errors.Is(err, ErrIndexExists) { return true } suite.T().Logf("Unexpected error, retrying: %v", err) return false }) suite.tryUntil(time.Now().Add(5*time.Second), 100*time.Millisecond, func() bool { err = mgr.CreatePrimaryIndex(bucketName, &CreatePrimaryQueryIndexOptions{ Deferred: true, ScopeName: "_default", CollectionName: collectionName, }) if err == nil || errors.Is(err, ErrIndexExists) { return true } suite.T().Logf("Unexpected error, retrying: %v", err) return false }) names, err := mgr.BuildDeferredIndexes(bucketName, &BuildDeferredQueryIndexOptions{ ScopeName: "_default", CollectionName: collectionName, }) suite.Require().Nil(err, err) suite.Assert().Equal([]string{"#primary"}, names) suite.Eventually(func() bool { indexes, err := mgr.GetAllIndexes(bucketName, nil) suite.Require().Nil(err, err) suite.Assert().Len(indexes, 2) for _, index := range indexes { if index.CollectionName == "" { suite.Assert().Equal("deferred", index.State) } else { if !(index.State == "building" || index.State == "online") { return false } } } return true }, 5*time.Second, 100*time.Millisecond) } func (suite *IntegrationTestSuite) TestQueryIndexesIncludesDefaultCollection() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) bucketMgr := globalCluster.Buckets() bucketName := "testIndexes" + uuid.NewString() err := bucketMgr.CreateBucket(CreateBucketSettings{ BucketSettings: BucketSettings{ Name: bucketName, RAMQuotaMB: 100, NumReplicas: 0, BucketType: CouchbaseBucketType, }, }, nil) suite.Require().Nil(err, err) defer bucketMgr.DropBucket(bucketName, nil) mgr := globalCluster.QueryIndexes() deadline := time.Now().Add(5 * time.Second) for { err = mgr.CreateIndex(bucketName, "myindex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: true, }) if err == nil { break } suite.T().Logf("Failed to create index: %s", err) sleepDeadline := time.Now().Add(500 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for create index to succeed") return } } indexes, err := mgr.GetAllIndexes(bucketName, &GetAllQueryIndexesOptions{ ScopeName: "_default", }) suite.Require().Nil(err, err) suite.Require().Len(indexes, 1) var index QueryIndex for _, idx := range indexes { if idx.Name == "myindex" { index = idx break } } suite.Assert().Equal("myindex", index.Name) suite.Assert().False(index.IsPrimary) suite.Assert().Equal(QueryIndexTypeGsi, index.Type) suite.Assert().Equal("online", index.State) suite.Assert().Equal(bucketName, index.Keyspace) suite.Assert().Equal(bucketName, index.BucketName) suite.Assert().Equal("", index.ScopeName) suite.Assert().Equal("", index.CollectionName) suite.Assert().Equal("default", index.Namespace) if suite.Assert().Len(index.IndexKey, 1) { suite.Assert().Equal("`field`", index.IndexKey[0]) } suite.Assert().Empty(index.Condition) suite.Assert().Empty(index.Partition) } type testQueryIndexDataset struct { Results []map[string]interface{} jsonQueryResponse } type mockQueryIndexRowReader struct { Dataset []map[string]interface{} mockQueryRowReaderBase } func (arr *mockQueryIndexRowReader) NextRow() []byte { if arr.idx == len(arr.Dataset) { return nil } idx := arr.idx arr.idx++ return arr.Suite.mustConvertToBytes(arr.Dataset[idx]) } func (suite *UnitTestSuite) TestQueryIndexesParsing() { var dataset testQueryIndexDataset err := loadJSONTestDataset("query_index_response", &dataset) suite.Require().Nil(err, err) reader := &mockQueryIndexRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, }, } var cluster *Cluster cluster = suite.queryCluster(false, reader, nil) mgr := QueryIndexManager{ base: &baseQueryIndexManager{ provider: cluster, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, }, } res, err := mgr.GetAllIndexes("mybucket", nil) suite.Require().Nil(err, err) suite.Require().Len(res, 1) index := res[0] suite.Assert().Equal("ih", index.Name) suite.Assert().False(index.IsPrimary) suite.Assert().Equal(QueryIndexTypeGsi, index.Type) suite.Assert().Equal("online", index.State) suite.Assert().Equal("test", index.Keyspace) suite.Assert().Equal("default", index.Namespace) if suite.Assert().Len(index.IndexKey, 1) { suite.Assert().Equal("`_type`", index.IndexKey[0]) } suite.Assert().Empty(index.Condition) suite.Assert().Equal("HASH(`_type`)", index.Partition) } gocb-2.6.3/cluster_searchindexes.go000066400000000000000000000557051441755043100173710ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "strings" "time" ) type jsonSearchIndexResp struct { Status string `json:"status"` IndexDef *jsonSearchIndex `json:"indexDef"` } type jsonSearchIndexDefs struct { IndexDefs map[string]jsonSearchIndex `json:"indexDefs"` ImplVersion string `json:"implVersion"` } type jsonSearchIndexesResp struct { Status string `json:"status"` IndexDefs jsonSearchIndexDefs `json:"indexDefs"` } type jsonSearchIndex struct { UUID string `json:"uuid"` Name string `json:"name"` SourceName string `json:"sourceName"` Type string `json:"type"` Params map[string]interface{} `json:"params"` SourceUUID string `json:"sourceUUID"` SourceParams map[string]interface{} `json:"sourceParams"` SourceType string `json:"sourceType"` PlanParams map[string]interface{} `json:"planParams"` } // SearchIndex is used to define a search index. type SearchIndex struct { // UUID is required for updates. It provides a means of ensuring consistency, the UUID must match the UUID value // for the index on the server. UUID string // Name represents the name of this index. Name string // SourceName is the name of the source of the data for the index e.g. bucket name. SourceName string // Type is the type of index, e.g. fulltext-index or fulltext-alias. Type string // IndexParams are index properties such as store type and mappings. Params map[string]interface{} // SourceUUID is the UUID of the data source, this can be used to more tightly tie the index to a source. SourceUUID string // SourceParams are extra parameters to be defined. These are usually things like advanced connection and tuning // parameters. SourceParams map[string]interface{} // SourceType is the type of the data source, e.g. couchbase or nil depending on the Type field. SourceType string // PlanParams are plan properties such as number of replicas and number of partitions. PlanParams map[string]interface{} } func (si *SearchIndex) fromData(data jsonSearchIndex) error { si.UUID = data.UUID si.Name = data.Name si.SourceName = data.SourceName si.Type = data.Type si.Params = data.Params si.SourceUUID = data.SourceUUID si.SourceParams = data.SourceParams si.SourceType = data.SourceType si.PlanParams = data.PlanParams return nil } func (si *SearchIndex) toData() (jsonSearchIndex, error) { var data jsonSearchIndex data.UUID = si.UUID data.Name = si.Name data.SourceName = si.SourceName data.Type = si.Type data.Params = si.Params data.SourceUUID = si.SourceUUID data.SourceParams = si.SourceParams data.SourceType = si.SourceType data.PlanParams = si.PlanParams return data, nil } // SearchIndexManager provides methods for performing Couchbase search index management. type SearchIndexManager struct { mgmtProvider mgmtProvider tracer RequestTracer meter *meterWrapper } func (sm *SearchIndexManager) checkForRateLimitError(statusCode uint32, errMsg string) error { errMsg = strings.ToLower(errMsg) var err error if statusCode == 400 && strings.Contains(errMsg, "num_fts_indexes") { err = ErrQuotaLimitedFailure } else if statusCode == 429 { if strings.Contains(errMsg, "num_concurrent_requests") { err = ErrRateLimitedFailure } else if strings.Contains(errMsg, "num_queries_per_min") { err = ErrRateLimitedFailure } else if strings.Contains(errMsg, "ingress_mib_per_min") { err = ErrRateLimitedFailure } else if strings.Contains(errMsg, "egress_mib_per_min") { err = ErrRateLimitedFailure } } return err } func (sm *SearchIndexManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read search index response body: %s", err) return nil } if err := sm.checkForRateLimitError(resp.StatusCode, string(b)); err != nil { return makeGenericMgmtError(err, req, resp, string(b)) } var bodyErr error if strings.Contains(strings.ToLower(string(b)), "index not found") { bodyErr = ErrIndexNotFound } else if strings.Contains(strings.ToLower(string(b)), "index with the same name already exists") { bodyErr = ErrIndexExists } else { bodyErr = errors.New(string(b)) } return makeGenericMgmtError(bodyErr, req, resp, string(b)) } func (sm *SearchIndexManager) doMgmtRequest(ctx context.Context, req mgmtRequest) (*mgmtResponse, error) { resp, err := sm.mgmtProvider.executeMgmtRequest(ctx, req) if err != nil { return nil, err } return resp, nil } // GetAllSearchIndexOptions is the set of options available to the search indexes GetAllIndexes operation. type GetAllSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllIndexes retrieves all of the search indexes for the cluster. func (sm *SearchIndexManager) GetAllIndexes(opts *GetAllSearchIndexOptions) ([]SearchIndex, error) { if opts == nil { opts = &GetAllSearchIndexOptions{} } start := time.Now() defer sm.meter.ValueRecord(meterValueServiceManagement, "manager_search_get_all_indexes", start) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_get_all_indexes", "management") span.SetAttribute("db.operation", "GET /api/index") defer span.End() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "GET", Path: "/api/index", IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := sm.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return nil, idxErr } return nil, makeMgmtBadStatusError("failed to get index", &req, resp) } var indexesResp jsonSearchIndexesResp jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&indexesResp) if err != nil { return nil, err } indexDefs := indexesResp.IndexDefs.IndexDefs var indexes []SearchIndex for _, indexData := range indexDefs { var index SearchIndex err := index.fromData(indexData) if err != nil { return nil, err } indexes = append(indexes, index) } return indexes, nil } // GetSearchIndexOptions is the set of options available to the search indexes GetIndex operation. type GetSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetIndex retrieves a specific search index by name. func (sm *SearchIndexManager) GetIndex(indexName string, opts *GetSearchIndexOptions) (*SearchIndex, error) { if opts == nil { opts = &GetSearchIndexOptions{} } start := time.Now() defer sm.meter.ValueRecord(meterValueServiceManagement, "manager_search_get_index", start) path := fmt.Sprintf("/api/index/%s", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_get_index", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "GET", Path: path, IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := sm.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return nil, idxErr } return nil, makeMgmtBadStatusError("failed to get index", &req, resp) } var indexResp jsonSearchIndexResp jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&indexResp) if err != nil { return nil, err } var indexDef SearchIndex err = indexDef.fromData(*indexResp.IndexDef) if err != nil { return nil, err } return &indexDef, nil } // UpsertSearchIndexOptions is the set of options available to the search index manager UpsertIndex operation. type UpsertSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UpsertIndex creates or updates a search index. func (sm *SearchIndexManager) UpsertIndex(indexDefinition SearchIndex, opts *UpsertSearchIndexOptions) error { if opts == nil { opts = &UpsertSearchIndexOptions{} } if indexDefinition.Name == "" { return invalidArgumentsError{"index name cannot be empty"} } if indexDefinition.Type == "" { return invalidArgumentsError{"index type cannot be empty"} } start := time.Now() defer sm.meter.ValueRecord(meterValueServiceManagement, "manager_search_upsert_index", start) path := fmt.Sprintf("/api/index/%s", indexDefinition.Name) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_upsert_index", "management") span.SetAttribute("db.operation", "PUT "+path) defer span.End() indexData, err := indexDefinition.toData() if err != nil { return err } b, err := json.Marshal(indexData) if err != nil { return err } req := mgmtRequest{ Service: ServiceTypeSearch, Method: "PUT", Path: path, Headers: map[string]string{ "cache-control": "no-cache", }, Body: b, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := sm.doMgmtRequest(opts.Context, req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return idxErr } return makeMgmtBadStatusError("failed to create index", &req, resp) } return nil } // DropSearchIndexOptions is the set of options available to the search index DropIndex operation. type DropSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropIndex removes the search index with the specific name. func (sm *SearchIndexManager) DropIndex(indexName string, opts *DropSearchIndexOptions) error { if opts == nil { opts = &DropSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } start := time.Now() defer sm.meter.ValueRecord(meterValueServiceManagement, "manager_search_drop_index", start) path := fmt.Sprintf("/api/index/%s", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_drop_index", "management") span.SetAttribute("db.operation", "DELETE "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "DELETE", Path: path, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := sm.doMgmtRequest(opts.Context, req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { return makeMgmtBadStatusError("failed to drop the index", &req, resp) } return nil } // AnalyzeDocumentOptions is the set of options available to the search index AnalyzeDocument operation. type AnalyzeDocumentOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // AnalyzeDocument returns how a doc is analyzed against a specific index. func (sm *SearchIndexManager) AnalyzeDocument(indexName string, doc interface{}, opts *AnalyzeDocumentOptions) ([]interface{}, error) { if opts == nil { opts = &AnalyzeDocumentOptions{} } if indexName == "" { return nil, invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/analyzeDoc", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_analyze_document", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() b, err := json.Marshal(doc) if err != nil { return nil, err } req := mgmtRequest{ Service: ServiceTypeSearch, Method: "POST", Path: path, Body: b, IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := sm.doMgmtRequest(opts.Context, req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return nil, idxErr } return nil, makeMgmtBadStatusError("failed to analyze document", &req, resp) } var analysis struct { Status string `json:"status"` Analyzed []interface{} `json:"analyzed"` } jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&analysis) if err != nil { return nil, err } return analysis.Analyzed, nil } // GetIndexedDocumentsCountOptions is the set of options available to the search index GetIndexedDocumentsCount operation. type GetIndexedDocumentsCountOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetIndexedDocumentsCount retrieves the document count for a search index. func (sm *SearchIndexManager) GetIndexedDocumentsCount(indexName string, opts *GetIndexedDocumentsCountOptions) (uint64, error) { if opts == nil { opts = &GetIndexedDocumentsCountOptions{} } if indexName == "" { return 0, invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/count", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_get_indexed_documents_count", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "GET", Path: path, IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := sm.doMgmtRequest(opts.Context, req) if err != nil { return 0, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return 0, idxErr } return 0, makeMgmtBadStatusError("failed to get the indexed documents count", &req, resp) } var count struct { Count uint64 `json:"count"` } jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&count) if err != nil { return 0, err } return count.Count, nil } func (sm *SearchIndexManager) performControlRequest( ctx context.Context, tracectx RequestSpanContext, method, uri string, timeout time.Duration, retryStrategy RetryStrategy, ) error { req := mgmtRequest{ Service: ServiceTypeSearch, Method: method, Path: uri, IsIdempotent: true, Timeout: timeout, RetryStrategy: retryStrategy, parentSpanCtx: tracectx, } resp, err := sm.doMgmtRequest(ctx, req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return idxErr } return makeMgmtBadStatusError("failed to perform the control request", &req, resp) } return nil } // PauseIngestSearchIndexOptions is the set of options available to the search index PauseIngest operation. type PauseIngestSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // PauseIngest pauses updates and maintenance for an index. func (sm *SearchIndexManager) PauseIngest(indexName string, opts *PauseIngestSearchIndexOptions) error { if opts == nil { opts = &PauseIngestSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/ingestControl/pause", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_pause_ingest", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() return sm.performControlRequest( opts.Context, span.Context(), "POST", path, opts.Timeout, opts.RetryStrategy) } // ResumeIngestSearchIndexOptions is the set of options available to the search index ResumeIngest operation. type ResumeIngestSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // ResumeIngest resumes updates and maintenance for an index. func (sm *SearchIndexManager) ResumeIngest(indexName string, opts *ResumeIngestSearchIndexOptions) error { if opts == nil { opts = &ResumeIngestSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/ingestControl/resume", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_resume_ingest", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() return sm.performControlRequest( opts.Context, span.Context(), "POST", path, opts.Timeout, opts.RetryStrategy) } // AllowQueryingSearchIndexOptions is the set of options available to the search index AllowQuerying operation. type AllowQueryingSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // AllowQuerying allows querying against an index. func (sm *SearchIndexManager) AllowQuerying(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/queryControl/allow", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_allow_querying", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() return sm.performControlRequest( opts.Context, span.Context(), "POST", path, opts.Timeout, opts.RetryStrategy) } // DisallowQueryingSearchIndexOptions is the set of options available to the search index DisallowQuerying operation. type DisallowQueryingSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DisallowQuerying disallows querying against an index. func (sm *SearchIndexManager) DisallowQuerying(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/queryControl/disallow", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_disallow_querying", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() return sm.performControlRequest( opts.Context, span.Context(), "POST", path, opts.Timeout, opts.RetryStrategy) } // FreezePlanSearchIndexOptions is the set of options available to the search index FreezePlan operation. type FreezePlanSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // FreezePlan freezes the assignment of index partitions to nodes. func (sm *SearchIndexManager) FreezePlan(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/planFreezeControl/freeze", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_freeze_plan", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() return sm.performControlRequest( opts.Context, span.Context(), "POST", path, opts.Timeout, opts.RetryStrategy) } // UnfreezePlanSearchIndexOptions is the set of options available to the search index UnfreezePlan operation. type UnfreezePlanSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UnfreezePlan unfreezes the assignment of index partitions to nodes. func (sm *SearchIndexManager) UnfreezePlan(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } path := fmt.Sprintf("/api/index/%s/planFreezeControl/unfreeze", indexName) span := createSpan(sm.tracer, opts.ParentSpan, "manager_search_unfreeze_plan", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() return sm.performControlRequest( opts.Context, span.Context(), "POST", path, opts.Timeout, opts.RetryStrategy) } gocb-2.6.3/cluster_searchindexes_test.go000066400000000000000000000172421441755043100204220ustar00rootroot00000000000000package gocb import ( "bytes" "errors" "fmt" "github.com/google/uuid" "io/ioutil" "time" "github.com/stretchr/testify/mock" ) func (suite *IntegrationTestSuite) newSearchIndexName() string { indexName := "a" + uuid.New().String() return indexName } func (suite *IntegrationTestSuite) TestSearchIndexesCrud() { suite.skipIfUnsupported(SearchIndexFeature) suite.skipIfServerVersionEquals(srvVer750) mgr := globalCluster.SearchIndexes() indexName := suite.newSearchIndexName() err := mgr.UpsertIndex(SearchIndex{ Name: indexName, Type: "fulltext-index", SourceType: "couchbase", SourceName: globalBucket.Name(), }, nil) if err != nil { suite.T().Fatalf("Expected UpsertIndex err to be nil but was %v", err) } // Upsert requires a UUID. err = mgr.UpsertIndex(SearchIndex{ Name: indexName, Type: "fulltext-index", SourceType: "couchbase", SourceName: globalBucket.Name(), }, nil) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected UpsertIndex err to be already exists but was %v", err) } err = mgr.UpsertIndex(SearchIndex{ Name: "test2", Type: "fulltext-index", SourceType: "couchbase", SourceName: globalBucket.Name(), PlanParams: map[string]interface{}{ "indexPartitions": 1, }, Params: map[string]interface{}{ "store": map[string]string{ "indexType": "upside_down", "kvStoreName": "moss", }, }, }, nil) if err != nil { suite.T().Fatalf("Expected UpsertIndex err to be nil but was %v", err) } err = mgr.UpsertIndex(SearchIndex{ Name: "testAlias", Type: "fulltext-alias", SourceType: "nil", Params: map[string]interface{}{ "targets": map[string]interface{}{ "test": map[string]interface{}{}, "test2": map[string]interface{}{}, }, }, }, nil) if err != nil { suite.T().Fatalf("Expected UpsertIndexAlias err to be nil but was %v", err) } index, err := mgr.GetIndex(indexName, nil) if err != nil { suite.T().Fatalf("Expected GetIndex err to be nil but was %v", err) } _, err = mgr.GetIndex("testindexthatdoesnotexist", nil) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected GetIndex err to be not exists but was %v", err) } if index.Name != indexName { suite.T().Fatalf("Index name was not equal, expected test but was %v", index.Name) } if index.Type != "fulltext-index" { suite.T().Fatalf("Index type was not equal, expected fulltext-index but was %v", index.Type) } err = mgr.UpsertIndex(*index, &UpsertSearchIndexOptions{}) if err != nil { suite.T().Fatalf("Expected UpsertIndex err to be nil but was %v", err) } indexes, err := mgr.GetAllIndexes(nil) if err != nil { suite.T().Fatalf("Expected GetAll err to be nil but was %v", err) } if len(indexes) == 0 { suite.T().Fatalf("Expected GetAll to return more than 0 indexes") } err = mgr.DropIndex(indexName, nil) if err != nil { suite.T().Fatalf("Expected DropIndex err to be nil but was %v", err) } err = mgr.DropIndex("test2", nil) if err != nil { suite.T().Fatalf("Expected DropIndex err to be nil but was %v", err) } err = mgr.DropIndex("testAlias", nil) if err != nil { suite.T().Fatalf("Expected DropIndex err to be nil but was %v", err) } _, err = mgr.GetIndex("newTest", nil) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected GetIndex err to be not found but was %s", err) } _, err = mgr.GetIndex("test2", nil) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected GetIndex err to be not found but was %s", err) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_search_upsert_index"), 5, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_search_get_index"), 4, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_search_drop_index"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_search_get_all_indexes"), 1, false) } func (suite *IntegrationTestSuite) TestSearchIndexesUpsertIndexNoName() { suite.skipIfUnsupported(SearchIndexFeature) mgr := globalCluster.SearchIndexes() err := mgr.UpsertIndex(SearchIndex{}, nil) if err == nil { suite.T().Fatalf("Expected UpsertIndex err to be not nil but was") } if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected error to be InvalidArgument but was %v", err) } } func (suite *IntegrationTestSuite) TestSearchIndexesIngestControl() { suite.skipIfUnsupported(SearchIndexFeature) mgr := globalCluster.SearchIndexes() indexName := suite.newSearchIndexName() err := mgr.UpsertIndex(SearchIndex{ Name: indexName, Type: "fulltext-index", SourceType: "couchbase", SourceName: globalBucket.Name(), }, nil) if err != nil { suite.T().Fatalf("Expected UpsertIndex err to be nil but was %v", err) } defer mgr.DropIndex(indexName, nil) err = mgr.PauseIngest(indexName, nil) if err != nil { suite.T().Fatalf("Expected PauseIngest err to be nil but was %v", err) } err = mgr.ResumeIngest(indexName, nil) if err != nil { suite.T().Fatalf("Expected ResumeIngest err to be nil but was %v", err) } } func (suite *IntegrationTestSuite) TestSearchIndexesQueryControl() { suite.skipIfUnsupported(SearchIndexFeature) mgr := globalCluster.SearchIndexes() indexName := suite.newSearchIndexName() err := mgr.UpsertIndex(SearchIndex{ Name: indexName, Type: "fulltext-index", SourceType: "couchbase", SourceName: globalBucket.Name(), }, nil) if err != nil { suite.T().Fatalf("Expected UpsertIndex err to be nil but was %v", err) } defer mgr.DropIndex(indexName, nil) err = mgr.DisallowQuerying(indexName, nil) if err != nil { suite.T().Fatalf("Expected PauseIngest err to be nil but was %v", err) } err = mgr.AllowQuerying(indexName, nil) if err != nil { suite.T().Fatalf("Expected ResumeIngest err to be nil but was %v", err) } } func (suite *IntegrationTestSuite) TestSearchIndexesPartitionControl() { suite.skipIfUnsupported(SearchIndexFeature) mgr := globalCluster.SearchIndexes() indexName := suite.newSearchIndexName() err := mgr.UpsertIndex(SearchIndex{ Name: indexName, Type: "fulltext-index", SourceType: "couchbase", SourceName: globalBucket.Name(), }, nil) if err != nil { suite.T().Fatalf("Expected UpsertIndex err to be nil but was %v", err) } defer mgr.DropIndex(indexName, nil) err = mgr.FreezePlan(indexName, nil) if err != nil { suite.T().Fatalf("Expected PauseIngest err to be nil but was %v", err) } err = mgr.UnfreezePlan(indexName, nil) if err != nil { suite.T().Fatalf("Expected ResumeIngest err to be nil but was %v", err) } } func (suite *UnitTestSuite) TestSearchIndexesAnalyzeDocument() { analyzeResp, err := loadRawTestDataset("search_analyzedoc") suite.Require().Nil(err, err) resp := &mgmtResponse{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(analyzeResp)), } indexName := "searchy" mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal(fmt.Sprintf("/api/index/%s/analyzeDoc", indexName), req.Path) suite.Assert().Equal(ServiceTypeSearch, req.Service) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("POST", req.Method) suite.Assert().Nil(req.RetryStrategy) }). Return(resp, nil) mgr := SearchIndexManager{ mgmtProvider: mockProvider, tracer: &NoopTracer{}, } res, err := mgr.AnalyzeDocument(indexName, struct{}{}, &AnalyzeDocumentOptions{ Timeout: 1 * time.Second, }) suite.Require().Nil(err, err) suite.Require().NotNil(res) } gocb-2.6.3/cluster_searchquery.go000066400000000000000000000313501441755043100170650ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "time" cbsearch "github.com/couchbase/gocb/v2/search" gocbcore "github.com/couchbase/gocbcore/v10" ) type jsonRowLocation struct { Field string `json:"field"` Term string `json:"term"` Position uint32 `json:"position"` Start uint32 `json:"start"` End uint32 `json:"end"` ArrayPositions []uint32 `json:"array_positions"` } type jsonSearchTermFacet struct { Term string `json:"term,omitempty"` Count int `json:"count,omitempty"` } type jsonSearchNumericFacet struct { Name string `json:"name,omitempty"` Min float64 `json:"min,omitempty"` Max float64 `json:"max,omitempty"` Count int `json:"count,omitempty"` } type jsonSearchDateFacet struct { Name string `json:"name,omitempty"` Start string `json:"start,omitempty"` End string `json:"end,omitempty"` Count int `json:"count,omitempty"` } type jsonSearchFacet struct { Name string `json:"name"` Field string `json:"field"` Total uint64 `json:"total"` Missing uint64 `json:"missing"` Other uint64 `json:"other"` Terms []jsonSearchTermFacet `json:"terms"` NumericRanges []jsonSearchNumericFacet `json:"numeric_ranges"` DateRanges []jsonSearchDateFacet `json:"date_ranges"` } type jsonSearchRowLocations map[string]map[string][]jsonRowLocation type jsonSearchRow struct { Index string `json:"index"` ID string `json:"id"` Score float64 `json:"score"` Explanation interface{} `json:"explanation"` Locations jsonSearchRowLocations `json:"locations"` Fragments map[string][]string `json:"fragments"` Fields json.RawMessage `json:"fields"` } type jsonSearchResponseStatus struct { Errors map[string]string `json:"errors"` } type jsonSearchResponse struct { Status jsonSearchResponseStatus `json:"status,omitempty"` TotalHits uint64 `json:"total_hits"` MaxScore float64 `json:"max_score"` Took uint64 `json:"took"` Facets map[string]jsonSearchFacet `json:"facets"` } // SearchMetrics encapsulates various metrics gathered during a search queries execution. type SearchMetrics struct { Took time.Duration TotalRows uint64 MaxScore float64 TotalPartitionCount uint64 SuccessPartitionCount uint64 ErrorPartitionCount uint64 } func (metrics *SearchMetrics) fromData(data jsonSearchResponse) error { metrics.TotalRows = data.TotalHits metrics.MaxScore = data.MaxScore metrics.Took = time.Duration(data.Took) / time.Nanosecond return nil } // SearchMetaData provides access to the meta-data properties of a search query result. type SearchMetaData struct { Metrics SearchMetrics Errors map[string]string } func (meta *SearchMetaData) fromData(data jsonSearchResponse) error { metrics := SearchMetrics{} if err := metrics.fromData(data); err != nil { return err } meta.Metrics = metrics meta.Errors = data.Status.Errors return nil } // SearchTermFacetResult holds the results of a term facet in search results. type SearchTermFacetResult struct { Term string Count int } // SearchNumericRangeFacetResult holds the results of a numeric facet in search results. type SearchNumericRangeFacetResult struct { Name string Min float64 Max float64 Count int } // SearchDateRangeFacetResult holds the results of a date facet in search results. type SearchDateRangeFacetResult struct { Name string Start string End string Count int } // SearchFacetResult provides access to the result of a faceted query. type SearchFacetResult struct { Name string Field string Total uint64 Missing uint64 Other uint64 Terms []SearchTermFacetResult NumericRanges []SearchNumericRangeFacetResult DateRanges []SearchDateRangeFacetResult } func (fr *SearchFacetResult) fromData(data jsonSearchFacet) error { fr.Name = data.Name fr.Field = data.Field fr.Total = data.Total fr.Missing = data.Missing fr.Other = data.Other for _, term := range data.Terms { fr.Terms = append(fr.Terms, SearchTermFacetResult(term)) } for _, nr := range data.NumericRanges { fr.NumericRanges = append(fr.NumericRanges, SearchNumericRangeFacetResult(nr)) } for _, nr := range data.DateRanges { fr.DateRanges = append(fr.DateRanges, SearchDateRangeFacetResult(nr)) } return nil } // SearchRowLocation represents the location of a row match type SearchRowLocation struct { Position uint32 Start uint32 End uint32 ArrayPositions []uint32 } func (rl *SearchRowLocation) fromData(data jsonRowLocation) error { rl.Position = data.Position rl.Start = data.Start rl.End = data.End rl.ArrayPositions = data.ArrayPositions return nil } // SearchRow represents a single hit returned from a search query. type SearchRow struct { Index string ID string Score float64 Explanation interface{} Locations map[string]map[string][]SearchRowLocation Fragments map[string][]string fieldsBytes []byte } // Fields decodes the fields included in a search hit. func (sr *SearchRow) Fields(valuePtr interface{}) error { return json.Unmarshal(sr.fieldsBytes, valuePtr) } type searchRowReader interface { NextRow() []byte Err() error MetaData() ([]byte, error) Close() error } // SearchResultRaw provides raw access to search data. // VOLATILE: This API is subject to change at any time. type SearchResultRaw struct { reader searchRowReader } // NextBytes returns the next row as bytes. func (srr *SearchResultRaw) NextBytes() []byte { return srr.reader.NextRow() } // Err returns any errors that have occurred on the stream func (srr *SearchResultRaw) Err() error { err := srr.reader.Err() if err != nil { return maybeEnhanceSearchError(err) } return nil } // Close marks the results as closed, returning any errors that occurred during reading the results. func (srr *SearchResultRaw) Close() error { err := srr.reader.Close() if err != nil { return maybeEnhanceSearchError(err) } return nil } // MetaData returns any meta-data that was available from this query as bytes. func (srr *SearchResultRaw) MetaData() ([]byte, error) { return srr.reader.MetaData() } // SearchResult allows access to the results of a search query. type SearchResult struct { reader searchRowReader currentRow SearchRow jsonErr error } func newSearchResult(reader searchRowReader) *SearchResult { return &SearchResult{ reader: reader, } } // Raw returns a SearchResultRaw which can be used to access the raw byte data from search queries. // Calling this function invalidates the underlying SearchResult which will no longer be able to be used. // VOLATILE: This API is subject to change at any time. func (r *SearchResult) Raw() *SearchResultRaw { vr := &SearchResultRaw{ reader: r.reader, } r.reader = nil return vr } // Next assigns the next result from the results into the value pointer, returning whether the read was successful. func (r *SearchResult) Next() bool { if r.reader == nil { return false } rowBytes := r.reader.NextRow() if rowBytes == nil { return false } r.currentRow = SearchRow{} var rowData jsonSearchRow if err := json.Unmarshal(rowBytes, &rowData); err != nil { // This should never happen but if it does then lets store it in a best efforts basis and maybe the next // row will be ok. We can then return this from .Err(). r.jsonErr = err return true } r.currentRow.Index = rowData.Index r.currentRow.ID = rowData.ID r.currentRow.Score = rowData.Score r.currentRow.Explanation = rowData.Explanation r.currentRow.Fragments = rowData.Fragments r.currentRow.fieldsBytes = rowData.Fields locations := make(map[string]map[string][]SearchRowLocation) for fieldName, fieldData := range rowData.Locations { terms := make(map[string][]SearchRowLocation) for termName, termData := range fieldData { locations := make([]SearchRowLocation, len(termData)) for locIdx, locData := range termData { err := locations[locIdx].fromData(locData) if err != nil { logWarnf("failed to parse search query location data: %s", err) } } terms[termName] = locations } locations[fieldName] = terms } r.currentRow.Locations = locations return true } // Row returns the contents of the current row. func (r *SearchResult) Row() SearchRow { if r.reader == nil { return SearchRow{} } return r.currentRow } // Err returns any errors that have occurred on the stream func (r *SearchResult) Err() error { if r.reader == nil { return errors.New("result object is no longer valid") } err := r.reader.Err() if err != nil { return maybeEnhanceSearchError(err) } // This is an error from json unmarshal so no point in trying to enhance it. return r.jsonErr } // Close marks the results as closed, returning any errors that occurred during reading the results. func (r *SearchResult) Close() error { if r.reader == nil { return r.Err() } err := r.reader.Close() if err != nil { return maybeEnhanceSearchError(err) } return nil } func (r *SearchResult) getJSONResp() (jsonSearchResponse, error) { metaDataBytes, err := r.reader.MetaData() if err != nil { return jsonSearchResponse{}, err } var jsonResp jsonSearchResponse err = json.Unmarshal(metaDataBytes, &jsonResp) if err != nil { return jsonSearchResponse{}, err } return jsonResp, nil } // MetaData returns any meta-data that was available from this query. Note that // the meta-data will only be available once the object has been closed (either // implicitly or explicitly). func (r *SearchResult) MetaData() (*SearchMetaData, error) { if r.reader == nil { return nil, r.Err() } jsonResp, err := r.getJSONResp() if err != nil { return nil, err } var metaData SearchMetaData err = metaData.fromData(jsonResp) if err != nil { return nil, err } return &metaData, nil } // Facets returns any facets that were returned with this query. Note that the // facets will only be available once the object has been closed (either // implicitly or explicitly). func (r *SearchResult) Facets() (map[string]SearchFacetResult, error) { jsonResp, err := r.getJSONResp() if err != nil { return nil, err } facets := make(map[string]SearchFacetResult) for facetName, facetData := range jsonResp.Facets { var facet SearchFacetResult err := facet.fromData(facetData) if err != nil { return nil, err } facets[facetName] = facet } return facets, nil } // SearchQuery executes the analytics query statement on the server. func (c *Cluster) SearchQuery(indexName string, query cbsearch.Query, opts *SearchOptions) (*SearchResult, error) { if opts == nil { opts = &SearchOptions{} } start := time.Now() defer c.meter.ValueRecord(meterValueServiceSearch, "search", start) span := createSpan(c.tracer, opts.ParentSpan, "search", "search") span.SetAttribute("db.operation", indexName) defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = c.timeoutsConfig.SearchTimeout } deadline := time.Now().Add(timeout) retryStrategy := c.retryStrategyWrapper if opts.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(opts.RetryStrategy) } searchOpts, err := opts.toMap(indexName) if err != nil { return nil, SearchError{ InnerError: wrapError(err, "failed to generate query options"), Query: query, } } searchOpts["query"] = query return c.execSearchQuery(opts.Context, span, indexName, searchOpts, deadline, retryStrategy, opts.Internal.User) } func maybeGetSearchOptionQuery(options map[string]interface{}) interface{} { if value, ok := options["query"]; ok { return value } return "" } func (c *Cluster) execSearchQuery( ctx context.Context, span RequestSpan, indexName string, options map[string]interface{}, deadline time.Time, retryStrategy *retryStrategyWrapper, user string, ) (*SearchResult, error) { provider, err := c.getSearchProvider() if err != nil { return nil, SearchError{ InnerError: wrapError(err, "failed to get query provider"), Query: maybeGetSearchOptionQuery(options), } } eSpan := createSpan(c.tracer, span, "request_encoding", "") reqBytes, err := json.Marshal(options) eSpan.End() if err != nil { return nil, SearchError{ InnerError: wrapError(err, "failed to marshall query body"), Query: maybeGetSearchOptionQuery(options), } } res, err := provider.SearchQuery(ctx, gocbcore.SearchQueryOptions{ IndexName: indexName, Payload: reqBytes, RetryStrategy: retryStrategy, Deadline: deadline, TraceContext: span.Context(), User: user, }) if err != nil { return nil, maybeEnhanceSearchError(err) } return newSearchResult(res), nil } gocb-2.6.3/cluster_searchquery_test.go000066400000000000000000000340441441755043100201270ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "time" "github.com/couchbase/gocbcore/v10" "github.com/stretchr/testify/mock" "github.com/couchbase/gocb/v2/search" ) func (suite *IntegrationTestSuite) TestSearch() { suite.skipIfUnsupported(SearchFeature) n := suite.setupSearch() defer globalCluster.SearchIndexes().DropIndex("search_test_index", nil) suite.runSearchTest(n) } func (suite *IntegrationTestSuite) runSearchTest(n int) { deadline := time.Now().Add(60 * time.Second) query := search.NewTermQuery("search").Field("service") var result *SearchResult var rows []SearchRow for { globalTracer.Reset() globalMeter.Reset() var err error result, err = globalCluster.SearchQuery("search_test_index", query, &SearchOptions{ Timeout: 1 * time.Second, Facets: map[string]search.Facet{ "type": search.NewTermFacet("country", 5), "date": search.NewDateFacet("updated", 5).AddRange("updated", "2000-07-22 20:00:20", "2020-07-22 20:00:20"), "numeric": search.NewNumericFacet("geo.lat", 5).AddRange("lat", 30, 31), }, IncludeLocations: true, }) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(1, len(nilParents)) suite.AssertHTTPOpSpan(nilParents[0], "search", HTTPOpSpanExpectations{ operationID: "search_test_index", numDispatchSpans: 1, atLeastNumDispatchSpans: false, hasEncoding: true, service: "search", }) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "search", "search"), 1, false) if err != nil { sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Fatalf("timed out waiting for indexing") } continue } var thisRows []SearchRow for result.Next() { row := result.Row() thisRows = append(thisRows, row) } err = result.Err() suite.Require().Nil(err, err) if n == len(thisRows) { rows = thisRows break } sleepDeadline := time.Now().Add(1000 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Fatalf("timed out waiting for indexing") } } for _, row := range rows { if suite.Assert().Contains(row.Locations, "service") { if suite.Assert().Contains(row.Locations["service"], "search") { if suite.Assert().NotZero(row.Locations["service"]["search"]) { suite.Assert().Zero(row.Locations["service"]["search"][0].Start) suite.Assert().NotZero(row.Locations["service"]["search"][0].End) suite.Assert().Nil(row.Locations["service"]["search"][0].ArrayPositions) } } } } metadata, err := result.MetaData() suite.Require().Nil(err, err) suite.Assert().NotEmpty(metadata.Metrics.TotalRows) suite.Assert().NotEmpty(metadata.Metrics.Took) suite.Assert().NotEmpty(metadata.Metrics.MaxScore) facets, err := result.Facets() suite.Require().Nil(err, err) if suite.Assert().Contains(facets, "type") { f := facets["type"] suite.Assert().Equal("country", f.Field) suite.Assert().Equal(uint64(7), f.Total) suite.Assert().Equal(4, len(f.Terms)) for _, term := range f.Terms { switch term.Term { case "belgium": suite.Assert().Equal(2, term.Count) case "states": suite.Assert().Equal(2, term.Count) case "united": suite.Assert().Equal(2, term.Count) case "norway": suite.Assert().Equal(1, term.Count) default: suite.Failf("Unexpected facet term %s", term.Term) } } } if suite.Assert().Contains(facets, "date") { f := facets["date"] suite.Assert().Equal(uint64(5), f.Total) suite.Assert().Equal("updated", f.Field) suite.Assert().Equal(1, len(f.DateRanges)) suite.Assert().Equal(5, f.DateRanges[0].Count) suite.Assert().Equal("2000-07-22T20:00:20Z", f.DateRanges[0].Start) suite.Assert().Equal("2020-07-22T20:00:20Z", f.DateRanges[0].End) suite.Assert().Equal("updated", f.DateRanges[0].Name) } if suite.Assert().Contains(facets, "numeric") { f := facets["numeric"] suite.Assert().Equal(uint64(1), f.Total) suite.Assert().Equal("geo.lat", f.Field) suite.Assert().Equal(1, len(f.NumericRanges)) suite.Assert().Equal(1, f.NumericRanges[0].Count) suite.Assert().Equal(float64(30), f.NumericRanges[0].Min) suite.Assert().Equal(float64(31), f.NumericRanges[0].Max) suite.Assert().Equal("lat", f.NumericRanges[0].Name) } } func (suite *IntegrationTestSuite) setupSearch() int { n, err := suite.createBreweryDataset("beer_sample_brewery_five", "search", "", "") suite.Require().Nil(err, err) mgr := globalCluster.SearchIndexes() err = mgr.UpsertIndex(SearchIndex{ Name: "search_test_index", SourceName: globalBucket.Name(), SourceType: "couchbase", Type: "fulltext-index", }, &UpsertSearchIndexOptions{ Timeout: 1 * time.Second, }) suite.Require().Nil(err, err) return n } func (suite *IntegrationTestSuite) TestSearchContext() { suite.skipIfUnsupported(SearchFeature) ctx, cancel := context.WithCancel(context.Background()) cancel() res, err := globalCluster.SearchQuery("test", search.NewMatchAllQuery(), &SearchOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) defer cancel() res, err = globalCluster.SearchQuery("test", search.NewMatchAllQuery(), &SearchOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) } // We have to manually mock this because testify won't let return something which can iterate. type mockSearchRowReader struct { Dataset []jsonSearchRow Meta []byte MetaErr error CloseErr error RowsErr error Suite *UnitTestSuite idx int } func (arr *mockSearchRowReader) NextRow() []byte { if arr.idx == len(arr.Dataset) { return nil } idx := arr.idx arr.idx++ return arr.Suite.mustConvertToBytes(arr.Dataset[idx]) } func (arr *mockSearchRowReader) MetaData() ([]byte, error) { return arr.Meta, arr.MetaErr } func (arr *mockSearchRowReader) Close() error { return arr.CloseErr } func (arr *mockSearchRowReader) Err() error { return arr.RowsErr } type testSearchDataset struct { Hits []jsonSearchRow jsonSearchResponse } func (suite *UnitTestSuite) searchCluster(reader searchRowReader, runFn func(args mock.Arguments)) *Cluster { provider := new(mockSearchProvider) provider. On("SearchQuery", nil, mock.AnythingOfType("gocbcore.SearchQueryOptions")). Run(runFn). Return(reader, nil) cli := new(mockConnectionManager) cli.On("getSearchProvider").Return(provider, nil) cluster := suite.newCluster(cli) return cluster } func (suite *UnitTestSuite) TestSearchQuery() { var dataset testSearchDataset err := loadJSONTestDataset("beer_sample_search_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockSearchRowReader{ Dataset: dataset.Hits, Meta: suite.mustConvertToBytes(dataset.jsonSearchResponse), Suite: suite, } query := search.NewTermQuery("term").Field("field").Fuzziness(1).Boost(2).PrefixLength(3) var cluster *Cluster cluster = suite.searchCluster(reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.SearchQueryOptions) suite.Assert().Equal(cluster.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) if suite.Assert().Contains(actualOptions, "fields") { suite.Assert().Equal([]interface{}{"name"}, actualOptions["fields"]) } if suite.Assert().Contains(actualOptions, "query") { q := actualOptions["query"].(map[string]interface{}) suite.Assert().Equal("term", q["term"]) suite.Assert().Equal("field", q["field"]) suite.Assert().Equal(float64(1), q["fuzziness"]) suite.Assert().Equal(float64(2), q["boost"]) suite.Assert().Equal(float64(3), q["prefix_length"]) } if suite.Assert().Contains(actualOptions, "sort") { s := actualOptions["sort"].([]interface{}) suite.Require().Len(s, 1) srt := s[0].(map[string]interface{}) suite.Assert().Equal("id", srt["by"]) suite.Assert().Equal(true, srt["desc"]) } }) result, err := cluster.SearchQuery("testindex", query, &SearchOptions{ Fields: []string{"name"}, Facets: map[string]search.Facet{ "type": search.NewTermFacet("country", 5), }, Sort: []search.Sort{search.NewSearchSortID().Descending(true)}, }) suite.Require().Nil(err, err) suite.Require().NotNil(result) var hits []SearchRow for result.Next() { hit := result.Row() hits = append(hits, hit) var field struct { Name string } err := hit.Fields(&field) suite.Require().Nil(err, err) suite.Assert().NotEmpty(field.Name) } err = result.Err() suite.Require().Nil(err, err) suite.Assert().Len(hits, len(dataset.Hits)) metadata, err := result.MetaData() suite.Require().Nil(err, err) suite.Assert().Nil(metadata.Errors) suite.Assert().Equal(uint64(809), metadata.Metrics.TotalRows) suite.Assert().Equal(1.156383395549805, metadata.Metrics.MaxScore) suite.Assert().Equal(time.Duration(62511375), metadata.Metrics.Took) facets, err := result.Facets() suite.Require().Nil(err, err) expectedFacets := make(map[string]SearchFacetResult) for facetName, facetData := range dataset.Facets { var facet SearchFacetResult err := facet.fromData(facetData) suite.Require().Nil(err, err) expectedFacets[facetName] = facet } suite.Assert().Equal(expectedFacets, facets) } func (suite *UnitTestSuite) TestSearchQueryDisableScoring() { reader := &mockSearchRowReader{ Dataset: []jsonSearchRow{}, Meta: []byte{}, Suite: suite, } query := search.NewMatchAllQuery() var cluster *Cluster cluster = suite.searchCluster(reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.SearchQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) if suite.Assert().Contains(actualOptions, "score") { suite.Assert().Equal("none", actualOptions["score"]) } }) _, err := cluster.SearchQuery("testindex", query, &SearchOptions{ DisableScoring: true, }) suite.Require().Nil(err, err) } func (suite *UnitTestSuite) TestSearchQueryNoScoringSet() { reader := &mockSearchRowReader{ Dataset: []jsonSearchRow{}, Meta: []byte{}, Suite: suite, } query := search.NewMatchAllQuery() var cluster *Cluster cluster = suite.searchCluster(reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.SearchQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().NotContains(actualOptions, "score") }) _, err := cluster.SearchQuery("testindex", query, &SearchOptions{}) suite.Require().Nil(err, err) } func (suite *UnitTestSuite) TestSearchQueryExplicitlyEnableScoring() { reader := &mockSearchRowReader{ Dataset: []jsonSearchRow{}, Meta: []byte{}, Suite: suite, } query := search.NewMatchAllQuery() var cluster *Cluster cluster = suite.searchCluster(reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.SearchQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().NotContains(actualOptions, "score") }) _, err := cluster.SearchQuery("testindex", query, &SearchOptions{ DisableScoring: false, }) suite.Require().Nil(err, err) } func (suite *UnitTestSuite) TestSearchQueryRaw() { var dataset testSearchDataset err := loadJSONTestDataset("beer_sample_search_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockSearchRowReader{ Dataset: dataset.Hits, Meta: suite.mustConvertToBytes(dataset.jsonSearchResponse), Suite: suite, } query := search.NewTermQuery("term").Field("field").Fuzziness(1).Boost(2).PrefixLength(3) var cluster *Cluster cluster = suite.searchCluster(reader, func(args mock.Arguments) {}) result, err := cluster.SearchQuery("testindex", query, &SearchOptions{ Fields: []string{"name"}, Facets: map[string]search.Facet{ "type": search.NewTermFacet("country", 5), }, Sort: []search.Sort{search.NewSearchSortID().Descending(true)}, }) suite.Require().Nil(err, err) suite.Require().NotNil(result) raw := result.Raw() suite.Assert().False(result.Next()) suite.Assert().Error(result.Err()) suite.Assert().Error(result.Close()) suite.Assert().Zero(result.Row()) _, err = result.MetaData() suite.Assert().Error(err) var i int for b := raw.NextBytes(); b != nil; b = raw.NextBytes() { suite.Assert().Equal(suite.mustConvertToBytes(dataset.Hits[i]), b) i++ } err = raw.Err() suite.Require().Nil(err, err) metadata, err := raw.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(reader.Meta, metadata) } func (suite *UnitTestSuite) TestSearchQueryCollections() { reader := &mockSearchRowReader{ Dataset: []jsonSearchRow{}, Meta: []byte{}, Suite: suite, } query := search.NewMatchAllQuery() var cluster *Cluster cluster = suite.searchCluster(reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.SearchQueryOptions) var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Require().Contains(actualOptions, "collections") collections := actualOptions["collections"].([]interface{}) suite.Require().Len(collections, 2) suite.Assert().Equal("collection1", collections[0]) suite.Assert().Equal("collection2", collections[1]) }) _, err := cluster.SearchQuery("testindex", query, &SearchOptions{ Collections: []string{"collection1", "collection2"}, }) suite.Require().Nil(err, err) } gocb-2.6.3/cluster_test.go000066400000000000000000000064371441755043100155210ustar00rootroot00000000000000package gocb import ( "errors" "time" ) func (suite *IntegrationTestSuite) TestClusterWaitUntilReady() { suite.skipIfUnsupported(WaitUntilReadyFeature) suite.skipIfUnsupported(WaitUntilReadyClusterFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, }}) suite.Require().Nil(err, err) defer c.Close(nil) err = c.WaitUntilReady(7*time.Second, nil) suite.Require().Nil(err, err) // Just test that we can use the cluster. buckets, err := c.Buckets().GetAllBuckets(nil) suite.Require().Nil(err, err) suite.Assert().GreaterOrEqual(len(buckets), 1) } func (suite *IntegrationTestSuite) TestClusterWaitUntilReadyInvalidAuth() { suite.skipIfUnsupported(WaitUntilReadyFeature) suite.skipIfUnsupported(WaitUntilReadyClusterFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password + "nopethisshouldntwork", }}) suite.Require().Nil(err, err) defer c.Close(nil) start := time.Now() err = c.WaitUntilReady(7*time.Second, nil) if !errors.Is(err, ErrUnambiguousTimeout) { suite.T().Fatalf("Expected unambiguous timeout error but was %v", err) } elapsed := time.Since(start) suite.Assert().GreaterOrEqual(int64(elapsed), int64(7*time.Second)) suite.Assert().LessOrEqual(int64(elapsed), int64(8*time.Second)) } func (suite *IntegrationTestSuite) TestClusterWaitUntilReadyFastFailAuth() { suite.skipIfUnsupported(WaitUntilReadyFeature) suite.skipIfUnsupported(WaitUntilReadyClusterFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: "thisisaprettyunlikelypasswordtobeused", }}) suite.Require().Nil(err, err) defer c.Close(nil) err = c.WaitUntilReady(7*time.Second, &WaitUntilReadyOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if !errors.Is(err, ErrAuthenticationFailure) { suite.T().Fatalf("Expected authentication error but was: %v", err) } } func (suite *IntegrationTestSuite) TestClusterWaitUntilReadyFastFailConnStr() { suite.skipIfUnsupported(WaitUntilReadyFeature) suite.skipIfUnsupported(WaitUntilReadyClusterFeature) c, err := Connect("10.10.10.10", ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, }}) suite.Require().Nil(err, err) defer c.Close(nil) err = c.WaitUntilReady(7*time.Second, &WaitUntilReadyOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if !errors.Is(err, ErrTimeout) { suite.T().Fatalf("Expected timeout error but was: %v", err) } } func (suite *IntegrationTestSuite) TestClusterWaitUntilReadyKeyValueService() { suite.skipIfUnsupported(WaitUntilReadyFeature) suite.skipIfUnsupported(WaitUntilReadyClusterFeature) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, }}) suite.Require().Nil(err, err) defer c.Close(nil) err = c.WaitUntilReady(7*time.Second, &WaitUntilReadyOptions{ ServiceTypes: []ServiceType{ServiceTypeKeyValue}, }) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected error to be invalid argument but was %v", err) } } gocb-2.6.3/cluster_usermgr.go000066400000000000000000000655761441755043100162370ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "net/url" "strings" "time" "github.com/google/uuid" ) // AuthDomain specifies the user domain of a specific user type AuthDomain string const ( // LocalDomain specifies users that are locally stored in Couchbase. LocalDomain AuthDomain = "local" // ExternalDomain specifies users that are externally stored // (in LDAP for instance). ExternalDomain AuthDomain = "external" ) type jsonOrigin struct { Type string `json:"type"` Name string `json:"name"` } type jsonRole struct { RoleName string `json:"role"` BucketName string `json:"bucket_name"` ScopeName string `json:"scope_name"` CollectionName string `json:"collection_name"` } type jsonRoleDescription struct { jsonRole Name string `json:"name"` Description string `json:"desc"` } type jsonRoleOrigins struct { jsonRole Origins []jsonOrigin } type jsonUserMetadata struct { ID string `json:"id"` Name string `json:"name"` Roles []jsonRoleOrigins `json:"roles"` Groups []string `json:"groups"` Domain AuthDomain `json:"domain"` ExternalGroups []string `json:"external_groups"` PasswordChanged time.Time `json:"password_change_date"` } type jsonGroup struct { Name string `json:"id"` Description string `json:"description"` Roles []jsonRole `json:"roles"` LDAPGroupReference string `json:"ldap_group_ref"` } // Role represents a specific permission. type Role struct { Name string `json:"role"` Bucket string `json:"bucket_name"` Scope string `json:"scope_name"` Collection string `json:"collection_name"` } func (ro *Role) fromData(data jsonRole) error { ro.Name = data.RoleName ro.Bucket = data.BucketName ro.Scope = data.ScopeName ro.Collection = data.CollectionName if ro.Scope == "*" { ro.Scope = "" } if ro.Collection == "*" { ro.Collection = "" } return nil } // RoleAndDescription represents a role with its display name and description. type RoleAndDescription struct { Role DisplayName string Description string } func (rd *RoleAndDescription) fromData(data jsonRoleDescription) error { err := rd.Role.fromData(data.jsonRole) if err != nil { return err } rd.DisplayName = data.Name rd.Description = data.Description return nil } // Origin indicates why a user has a specific role. Is the Origin Type is "user" then the role is assigned // directly to the user. If the type is "group" then it means that the role has been inherited from the group // identified by the Name field. type Origin struct { Type string Name string } func (o *Origin) fromData(data jsonOrigin) error { o.Type = data.Type o.Name = data.Name return nil } // RoleAndOrigins associates a role with its origins. type RoleAndOrigins struct { Role Origins []Origin } func (ro *RoleAndOrigins) fromData(data jsonRoleOrigins) error { err := ro.Role.fromData(data.jsonRole) if err != nil { return err } origins := make([]Origin, len(data.Origins)) for i, originData := range data.Origins { var origin Origin err := origin.fromData(originData) if err != nil { return err } origins[i] = origin } ro.Origins = origins return nil } // User represents a user which was retrieved from the server. type User struct { Username string DisplayName string // Roles are the roles assigned to the user that are of type "user". Roles []Role Groups []string Password string } // UserAndMetadata represents a user and user meta-data from the server. type UserAndMetadata struct { User Domain AuthDomain // EffectiveRoles are all of the user's roles and the origins. EffectiveRoles []RoleAndOrigins ExternalGroups []string PasswordChanged time.Time } func (um *UserAndMetadata) fromData(data jsonUserMetadata) error { um.User.Username = data.ID um.User.DisplayName = data.Name um.User.Groups = data.Groups um.ExternalGroups = data.ExternalGroups um.Domain = data.Domain um.PasswordChanged = data.PasswordChanged var roles []Role var effectiveRoles []RoleAndOrigins for _, roleData := range data.Roles { var effectiveRole RoleAndOrigins err := effectiveRole.fromData(roleData) if err != nil { return err } effectiveRoles = append(effectiveRoles, effectiveRole) role := effectiveRole.Role if roleData.Origins == nil { roles = append(roles, role) } else { for _, origin := range effectiveRole.Origins { if origin.Type == "user" { roles = append(roles, role) break } } } } um.EffectiveRoles = effectiveRoles um.User.Roles = roles return nil } // Group represents a user group on the server. type Group struct { Name string Description string Roles []Role LDAPGroupReference string } func (g *Group) fromData(data jsonGroup) error { g.Name = data.Name g.Description = data.Description g.LDAPGroupReference = data.LDAPGroupReference roles := make([]Role, len(data.Roles)) for roleIdx, roleData := range data.Roles { err := roles[roleIdx].fromData(roleData) if err != nil { return err } } g.Roles = roles return nil } // UserManager provides methods for performing Couchbase user management. type UserManager struct { provider mgmtProvider tracer RequestTracer meter *meterWrapper } func (um *UserManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read search index response body: %s", err) return nil } var bodyErr error if resp.StatusCode == 404 { if strings.Contains(strings.ToLower(string(b)), "unknown user") { bodyErr = ErrUserNotFound } else if strings.Contains(strings.ToLower(string(b)), "user was not found") { bodyErr = ErrUserNotFound } else if strings.Contains(strings.ToLower(string(b)), "group was not found") { bodyErr = ErrGroupNotFound } else if strings.Contains(strings.ToLower(string(b)), "unknown group") { bodyErr = ErrGroupNotFound } else { bodyErr = errors.New(string(b)) } } else { if err := checkForRateLimitError(resp.StatusCode, string(b)); err != nil { return makeGenericMgmtError(err, req, resp, string(b)) } bodyErr = errors.New(string(b)) } return makeGenericMgmtError(bodyErr, req, resp, string(b)) } // GetAllUsersOptions is the set of options available to the user manager GetAll operation. type GetAllUsersOptions struct { Timeout time.Duration RetryStrategy RetryStrategy DomainName string ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllUsers returns a list of all the users from the cluster. func (um *UserManager) GetAllUsers(opts *GetAllUsersOptions) ([]UserAndMetadata, error) { if opts == nil { opts = &GetAllUsersOptions{} } if opts.DomainName == "" { opts.DomainName = string(LocalDomain) } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_get_all_users", start) path := fmt.Sprintf("/settings/rbac/users/%s", opts.DomainName) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_get_all_users", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "GET", Path: path, IsIdempotent: true, RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return nil, usrErr } return nil, makeMgmtBadStatusError("failed to get users", &req, resp) } var usersData []jsonUserMetadata jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&usersData) if err != nil { return nil, err } users := make([]UserAndMetadata, len(usersData)) for userIdx, userData := range usersData { err := users[userIdx].fromData(userData) if err != nil { return nil, err } } return users, nil } // GetUserOptions is the set of options available to the user manager Get operation. type GetUserOptions struct { Timeout time.Duration RetryStrategy RetryStrategy DomainName string ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetUser returns the data for a particular user func (um *UserManager) GetUser(name string, opts *GetUserOptions) (*UserAndMetadata, error) { if opts == nil { opts = &GetUserOptions{} } if opts.DomainName == "" { opts.DomainName = string(LocalDomain) } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_get_user", start) path := fmt.Sprintf("/settings/rbac/users/%s/%s", opts.DomainName, name) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_get_user", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "GET", Path: path, IsIdempotent: true, RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return nil, usrErr } return nil, makeMgmtBadStatusError("failed to get user", &req, resp) } var userData jsonUserMetadata jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&userData) if err != nil { return nil, err } var user UserAndMetadata err = user.fromData(userData) if err != nil { return nil, err } return &user, nil } // UpsertUserOptions is the set of options available to the user manager Upsert operation. type UpsertUserOptions struct { Timeout time.Duration RetryStrategy RetryStrategy DomainName string ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UpsertUser updates a built-in RBAC user on the cluster. func (um *UserManager) UpsertUser(user User, opts *UpsertUserOptions) error { if opts == nil { opts = &UpsertUserOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_upsert_user", start) if opts.DomainName == "" { opts.DomainName = string(LocalDomain) } parseWildcard := func(str string) string { if str == "*" { return "" } return str } isNullOrWildcard := func(str string) bool { if str == "*" || str == "" { return true } return false } path := fmt.Sprintf("/settings/rbac/users/%s/%s", opts.DomainName, user.Username) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_upsert_user", "management") span.SetAttribute("db.operation", "PUT "+path) defer span.End() var reqRoleStrs []string for _, roleData := range user.Roles { if roleData.Bucket == "" { reqRoleStrs = append(reqRoleStrs, roleData.Name) } else { scope := parseWildcard(roleData.Scope) collection := parseWildcard(roleData.Collection) if scope != "" && isNullOrWildcard(roleData.Bucket) { return makeInvalidArgumentsError("when a scope is specified, the bucket cannot be null or wildcard") } if collection != "" && isNullOrWildcard(scope) { return makeInvalidArgumentsError("when a collection is specified, the scope cannot be null or wildcard") } roleStr := fmt.Sprintf("%s[%s", roleData.Name, roleData.Bucket) if scope != "" { roleStr += ":" + roleData.Scope } if collection != "" { roleStr += ":" + roleData.Collection } roleStr += "]" reqRoleStrs = append(reqRoleStrs, roleStr) } } reqForm := make(url.Values) reqForm.Add("name", user.DisplayName) if user.Password != "" { reqForm.Add("password", user.Password) } if len(user.Groups) > 0 { reqForm.Add("groups", strings.Join(user.Groups, ",")) } reqForm.Add("roles", strings.Join(reqRoleStrs, ",")) req := mgmtRequest{ Service: ServiceTypeManagement, Method: "PUT", Path: path, Body: []byte(reqForm.Encode()), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return usrErr } return makeMgmtBadStatusError("failed to upsert user", &req, resp) } return nil } // DropUserOptions is the set of options available to the user manager Drop operation. type DropUserOptions struct { Timeout time.Duration RetryStrategy RetryStrategy DomainName string ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropUser removes a built-in RBAC user on the cluster. func (um *UserManager) DropUser(name string, opts *DropUserOptions) error { if opts == nil { opts = &DropUserOptions{} } if opts.DomainName == "" { opts.DomainName = string(LocalDomain) } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_drop_user", start) path := fmt.Sprintf("/settings/rbac/users/%s/%s", opts.DomainName, name) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_drop_user", "management") span.SetAttribute("db.operation", "DELETE "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "DELETE", Path: path, RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return usrErr } return makeMgmtBadStatusError("failed to drop user", &req, resp) } return nil } // GetRolesOptions is the set of options available to the user manager GetRoles operation. type GetRolesOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetRoles lists the roles supported by the cluster. func (um *UserManager) GetRoles(opts *GetRolesOptions) ([]RoleAndDescription, error) { if opts == nil { opts = &GetRolesOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_get_roles", start) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_get_roles", "management") span.SetAttribute("db.operation", "GET /settings/rbac/roles") defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "GET", Path: "/settings/rbac/roles", RetryStrategy: opts.RetryStrategy, IsIdempotent: true, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return nil, usrErr } return nil, makeMgmtBadStatusError("failed to get roles", &req, resp) } var roleDatas []jsonRoleDescription jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&roleDatas) if err != nil { return nil, err } roles := make([]RoleAndDescription, len(roleDatas)) for roleIdx, roleData := range roleDatas { err := roles[roleIdx].fromData(roleData) if err != nil { return nil, err } } return roles, nil } // GetGroupOptions is the set of options available to the group manager Get operation. type GetGroupOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetGroup fetches a single group from the server. func (um *UserManager) GetGroup(groupName string, opts *GetGroupOptions) (*Group, error) { if groupName == "" { return nil, makeInvalidArgumentsError("groupName cannot be empty") } if opts == nil { opts = &GetGroupOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_get_group", start) path := fmt.Sprintf("/settings/rbac/groups/%s", groupName) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_get_group", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "GET", Path: path, RetryStrategy: opts.RetryStrategy, IsIdempotent: true, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return nil, usrErr } return nil, makeMgmtBadStatusError("failed to get group", &req, resp) } var groupData jsonGroup jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&groupData) if err != nil { return nil, err } var group Group err = group.fromData(groupData) if err != nil { return nil, err } return &group, nil } // GetAllGroupsOptions is the set of options available to the group manager GetAll operation. type GetAllGroupsOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // GetAllGroups fetches all groups from the server. func (um *UserManager) GetAllGroups(opts *GetAllGroupsOptions) ([]Group, error) { if opts == nil { opts = &GetAllGroupsOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_get_all_groups", start) path := "/settings/rbac/groups" span := createSpan(um.tracer, opts.ParentSpan, "manager_users_get_all_groups", "management") span.SetAttribute("db.operation", "GET "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "GET", Path: path, RetryStrategy: opts.RetryStrategy, IsIdempotent: true, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return nil, makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return nil, usrErr } return nil, makeMgmtBadStatusError("failed to get all groups", &req, resp) } var groupDatas []jsonGroup jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&groupDatas) if err != nil { return nil, err } groups := make([]Group, len(groupDatas)) for groupIdx, groupData := range groupDatas { err = groups[groupIdx].fromData(groupData) if err != nil { return nil, err } } return groups, nil } // UpsertGroupOptions is the set of options available to the group manager Upsert operation. type UpsertGroupOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // UpsertGroup creates, or updates, a group on the server. func (um *UserManager) UpsertGroup(group Group, opts *UpsertGroupOptions) error { if group.Name == "" { return makeInvalidArgumentsError("group name cannot be empty") } if opts == nil { opts = &UpsertGroupOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_upsert_group", start) path := fmt.Sprintf("/settings/rbac/groups/%s", group.Name) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_upsert_group", "management") span.SetAttribute("db.operation", "PUT "+path) defer span.End() var reqRoleStrs []string for _, roleData := range group.Roles { if roleData.Bucket == "" { reqRoleStrs = append(reqRoleStrs, roleData.Name) } else { reqRoleStrs = append(reqRoleStrs, fmt.Sprintf("%s[%s]", roleData.Name, roleData.Bucket)) } } reqForm := make(url.Values) reqForm.Add("description", group.Description) reqForm.Add("ldap_group_ref", group.LDAPGroupReference) reqForm.Add("roles", strings.Join(reqRoleStrs, ",")) req := mgmtRequest{ Service: ServiceTypeManagement, Method: "PUT", Path: path, Body: []byte(reqForm.Encode()), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return usrErr } return makeMgmtBadStatusError("failed to upsert group", &req, resp) } return nil } // DropGroupOptions is the set of options available to the group manager Drop operation. type DropGroupOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // DropGroup removes a group from the server. func (um *UserManager) DropGroup(groupName string, opts *DropGroupOptions) error { if groupName == "" { return makeInvalidArgumentsError("groupName cannot be empty") } if opts == nil { opts = &DropGroupOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_drop_group", start) path := fmt.Sprintf("/settings/rbac/groups/%s", groupName) span := createSpan(um.tracer, opts.ParentSpan, "manager_users_drop_group", "management") span.SetAttribute("db.operation", "DELETE "+path) defer span.End() req := mgmtRequest{ Service: ServiceTypeManagement, Method: "DELETE", Path: path, RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return usrErr } return makeMgmtBadStatusError("failed to drop group", &req, resp) } return nil } // ChangePasswordOptions is the set of options available to the user manager ChangePassword operation. // UNCOMMITTED: This API may change in the future. type ChangePasswordOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // ChangePassword changes the password for the currently authenticated user. // *Note*: Usage of this function will effectively invalidate the SDK instance and further requests will fail // due to authentication errors. After using this function the SDK must be reinitialized. // UNCOMMITTED: This API may change in the future. func (um *UserManager) ChangePassword(newPassword string, opts *ChangePasswordOptions) error { if newPassword == "" { return makeInvalidArgumentsError("new password cannot be empty") } if opts == nil { opts = &ChangePasswordOptions{} } start := time.Now() defer um.meter.ValueRecord(meterValueServiceManagement, "manager_users_change_password", start) path := "/controller/changePassword" span := createSpan(um.tracer, opts.ParentSpan, "manager_users_change_password", "management") span.SetAttribute("db.operation", "POST "+path) defer span.End() reqForm := make(url.Values) reqForm.Add("password", newPassword) req := mgmtRequest{ Service: ServiceTypeManagement, Method: "POST", Path: path, Body: []byte(reqForm.Encode()), ContentType: "application/x-www-form-urlencoded", RetryStrategy: opts.RetryStrategy, UniqueID: uuid.New().String(), Timeout: opts.Timeout, parentSpanCtx: span.Context(), } resp, err := um.provider.executeMgmtRequest(opts.Context, req) if err != nil { return makeGenericMgmtError(err, &req, resp, "") } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { usrErr := um.tryParseErrorMessage(&req, resp) if usrErr != nil { return usrErr } return makeMgmtBadStatusError("failed to change password", &req, resp) } return nil } gocb-2.6.3/cluster_usermgr_test.go000066400000000000000000000444341441755043100172640ustar00rootroot00000000000000package gocb import ( "bytes" "errors" "io/ioutil" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/mock" ) func (suite *IntegrationTestSuite) TestUserManagerGroupCrud() { suite.skipIfUnsupported(UserGroupFeature) mgr := globalCluster.Users() err := mgr.UpsertGroup(Group{ Name: "test", Description: "this is a test", Roles: []Role{ { Name: "replication_target", Bucket: globalBucket.Name(), }, { Name: "replication_admin", }, }, LDAPGroupReference: "asda=price", }, nil) if err != nil { suite.T().Fatalf("Expected Upsert to not error: %v", err) } var group *Group suite.Require().Eventually(func() bool { group, err = mgr.GetGroup("test", nil) if err != nil { suite.T().Logf("Get errored: %v", err) return false } return true }, 5*time.Second, 100*time.Millisecond) group.Description = "this is still a test" group.Roles = append(group.Roles, Role{Name: "query_system_catalog"}) err = mgr.UpsertGroup(*group, nil) if err != nil { suite.T().Fatalf("Expected Upsert to not error: %v", err) } group.Name = "test2" err = mgr.UpsertGroup(*group, nil) if err != nil { suite.T().Fatalf("Expected Upsert to not error: %v", err) } groups, err := mgr.GetAllGroups(nil) if err != nil { suite.T().Fatalf("Expected GetAll to not error: %v", err) } if len(groups) < 2 { suite.T().Fatalf("Expected groups to contain at least 2 groups, was %v", groups) } roles, err := mgr.GetRoles(nil) if err != nil { suite.T().Fatalf("Expected GetAllClusterRoles to not error: %v", err) } suite.Assert().Greater(len(roles), 0) err = mgr.DropGroup("test", nil) if err != nil { suite.T().Fatalf("Expected Drop to not error: %v", err) } err = mgr.DropGroup("test2", nil) if err != nil { suite.T().Fatalf("Expected Drop to not error: %v", err) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_upsert_group"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_get_group"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_get_all_groups"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_get_roles"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_drop_group"), 2, false) } func (suite *IntegrationTestSuite) TestUserManagerWithGroupsCrud() { suite.skipIfUnsupported(UserGroupFeature) mgr := globalCluster.Users() err := mgr.UpsertGroup(Group{ Name: "test", Description: "this is a test", Roles: []Role{ { Name: "replication_target", Bucket: globalBucket.Name(), }, { Name: "replication_admin", }, }, }, nil) if err != nil { suite.T().Fatalf("Expected UpsertGroup to not error: %v", err) } expectedUser := User{ Username: "barry", DisplayName: "sheen", Password: "bangbang!", Roles: []Role{ { Name: "bucket_admin", Bucket: globalBucket.Name(), }, }, Groups: []string{"test"}, } err = mgr.UpsertUser(expectedUser, nil) if err != nil { suite.T().Fatalf("Expected UpsertUser to not error: %v", err) } var user *UserAndMetadata suite.Eventuallyf(func() bool { user, err = mgr.GetUser("barry", nil) if err != nil { suite.T().Logf("GetUser failed: %v", err) return false } return true }, 30*time.Second, 100*time.Millisecond, "GetUser failed to successfully return user") expectedUserAndMeta := &UserAndMetadata{ Domain: "local", User: expectedUser, EffectiveRoles: []RoleAndOrigins{ { Role: Role{ Name: "bucket_admin", Bucket: globalBucket.Name(), }, Origins: []Origin{ { Type: "user", }, }, }, { Role: Role{ Name: "replication_target", Bucket: globalBucket.Name(), }, Origins: []Origin{ { Type: "group", Name: "test", }, }, }, { Role: Role{ Name: "replication_admin", }, Origins: []Origin{ { Type: "group", Name: "test", }, }, }, }, } assertUserAndMetadata(suite.T(), user, expectedUserAndMeta) user.User.DisplayName = "barries" err = mgr.UpsertUser(user.User, nil) if err != nil { suite.T().Fatalf("Expected UpsertUser to not error: %v", err) } expectedUserAndMeta.User.DisplayName = "barries" assertUserAndMetadata(suite.T(), user, expectedUserAndMeta) users, err := mgr.GetAllUsers(nil) if err != nil { suite.T().Fatalf("Expected GetAllUsers to not error: %v", err) } if len(users) == 0 { suite.T().Fatalf("Expected users length to be greater than 0") } err = mgr.DropUser("barry", nil) if err != nil { suite.T().Fatalf("Expected DropUser to not error: %v", err) } err = mgr.DropGroup("test", nil) if err != nil { suite.T().Fatalf("Expected DropGroup to not error: %v", err) } _, err = mgr.GetUser("barry", nil) if !errors.Is(err, ErrUserNotFound) { suite.T().Fatalf("Expected error to be user not found but was %v", err) } } func (suite *IntegrationTestSuite) TestUserManagerCrud() { suite.skipIfUnsupported(UserManagerFeature) mgr := globalCluster.Users() expectedUser := User{ Username: "barry", DisplayName: "sheen", Password: "bangbang!", Roles: []Role{ { Name: "bucket_admin", Bucket: globalBucket.Name(), }, { Name: "replication_admin", }, { Name: "replication_target", Bucket: globalBucket.Name(), }, { Name: "cluster_admin", }, }, } err := mgr.UpsertUser(expectedUser, nil) if err != nil { suite.T().Fatalf("Expected UpsertUser to not error: %v", err) } var user *UserAndMetadata success := suite.tryUntil(time.Now().Add(5*time.Second), 50*time.Millisecond, func() bool { user, err = mgr.GetUser("barry", nil) if err != nil { suite.T().Logf("GetUser request errored with %s", err) return false } return true }) if !success { suite.T().Fatal("Wait time for get user expired") } expectedUserAndMeta := &UserAndMetadata{ Domain: "local", User: expectedUser, EffectiveRoles: []RoleAndOrigins{ { Role: Role{ Name: "bucket_admin", Bucket: globalBucket.Name(), }, }, { Role: Role{ Name: "replication_admin", }, }, { Role: Role{ Name: "replication_target", Bucket: globalBucket.Name(), }, }, { Role: Role{ Name: "cluster_admin", }, }, }, } assertUserAndMetadata(suite.T(), user, expectedUserAndMeta) user.User.DisplayName = "barries" success = suite.tryUntil(time.Now().Add(5*time.Second), 50*time.Millisecond, func() bool { err = mgr.UpsertUser(user.User, nil) if err != nil { suite.T().Logf("UpsertUser errored with %s", err) return false } return true }) if !success { suite.T().Fatal("Wait time for upsert user expired") } users, err := mgr.GetAllUsers(nil) if err != nil { suite.T().Fatalf("Expected GetAllUsers to not error: %v", err) } if len(users) == 0 { suite.T().Fatalf("Expected users length to be greater than 0") } success = suite.tryUntil(time.Now().Add(5*time.Second), 50*time.Millisecond, func() bool { user, err = mgr.GetUser("barry", nil) if err != nil { suite.T().Logf("GetUser errored with %s", err) return false } return true }) if !success { suite.T().Fatal("Wait time for get user expired") } expectedUserAndMeta.User.DisplayName = "barries" assertUserAndMetadata(suite.T(), user, expectedUserAndMeta) err = mgr.DropUser("barry", nil) if err != nil { suite.T().Fatalf("Expected DropUser to not error: %v", err) } suite.Require().Eventually(func() bool { _, err = mgr.GetUser("barry", nil) if !errors.Is(err, ErrUserNotFound) { suite.T().Logf("Expected error to be user not found but was %v", err) return false } return true }, 5*time.Second, 50*time.Millisecond) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_upsert_user"), 2, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_get_user"), 3, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_get_all_users"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_users_drop_user"), 1, false) } func (suite *IntegrationTestSuite) TestUserManagerAvailableRoles() { suite.skipIfUnsupported(UserManagerFeature) mgr := globalCluster.Users() roles, err := mgr.GetRoles(nil) if err != nil { suite.T().Fatalf("Expected GetRoles to not error %v", err) } if len(roles) == 0 { suite.T().Fatalf("Expected roles to have entries") } } func (suite *IntegrationTestSuite) TestUserManagerCollectionsRoles() { suite.skipIfUnsupported(UserManagerFeature) suite.skipIfUnsupported(CollectionsFeature) mgr := globalCluster.Users() type testCase struct { user User } testCases := []testCase{ { user: User{ Username: "collectionsUserBucket", DisplayName: "collections user bucket", Password: "password", Roles: []Role{ { Name: "data_reader", Bucket: globalBucket.Name(), }, }, }, }, { user: User{ Username: "collectionsUserScope", DisplayName: "collections user scope", Password: "password", Roles: []Role{ { Name: "data_reader", Bucket: globalBucket.Name(), Scope: globalScope.Name(), }, }, }, }, { user: User{ Username: "collectionsUserCollection", DisplayName: "collections user collection", Password: "password", Roles: []Role{ { Name: "data_reader", Bucket: globalBucket.Name(), Scope: globalScope.Name(), Collection: globalCollection.Name(), }, }, }, }, } for _, tCase := range testCases { suite.T().Run(tCase.user.Username, func(te *testing.T) { err := mgr.UpsertUser(tCase.user, nil) if err != nil { te.Logf("Expected err to be nil but was %v", err) te.Fail() return } found := suite.tryUntil(time.Now().Add(5*time.Second), 100*time.Millisecond, func() bool { user, err := mgr.GetUser(tCase.user.Username, nil) if err != nil { te.Logf("Expected err to be nil but was %v", err) return false } assertUser(te, &user.User, &tCase.user) return true }) if !found { te.Logf("User was not found") } }) } } func (suite *IntegrationTestSuite) TestUserManagerChangePassword() { suite.skipIfUnsupported(UserManagerFeature) suite.skipIfUnsupported(UserManagerChangePasswordFeature) username := uuid.NewString() password := "password" mgr := globalCluster.Users() err := mgr.UpsertUser(User{ Username: username, Password: password, Roles: []Role{ { Name: "data_reader", Bucket: globalBucket.Name(), }, }, }, nil) suite.Require().Nil(err, err) c, err := Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: username, Password: password, }}) suite.Require().Nil(err, err) closed := false defer func() { if !closed { c.Close(nil) } }() if globalCluster.SupportsFeature(WaitUntilReadyClusterFeature) { err = c.WaitUntilReady(20*time.Second, nil) suite.Require().Nil(err, err) } else { err = c.Bucket(globalConfig.Bucket).WaitUntilReady(20*time.Second, nil) suite.Require().Nil(err, err) } mgr = c.Users() newPassword := "newpassword" err = mgr.ChangePassword(newPassword, nil) suite.Require().Nil(err, err) err = c.Close(nil) closed = true suite.Require().Nil(err, err) c, err = Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: username, Password: newPassword, }}) suite.Require().Nil(err, err) defer c.Close(nil) if globalCluster.SupportsFeature(WaitUntilReadyClusterFeature) { err = c.WaitUntilReady(20*time.Second, nil) suite.Require().Nil(err, err) } else { err = c.Bucket(globalConfig.Bucket).WaitUntilReady(20*time.Second, nil) suite.Require().Nil(err, err) } c, err = Connect(globalConfig.connstr, ClusterOptions{Authenticator: PasswordAuthenticator{ Username: username, Password: password, }}) suite.Require().Nil(err, err) defer c.Close(nil) if globalCluster.SupportsFeature(WaitUntilReadyClusterFeature) { err = c.WaitUntilReady(10*time.Second, nil) suite.Require().NotNil(err, err) } else { err = c.Bucket(globalConfig.Bucket).WaitUntilReady(10*time.Second, nil) suite.Require().NotNil(err, err) } } func assertUser(t *testing.T, user *User, expected *User) { if user.Username != expected.Username { t.Logf("Expected user Username to be %s but was %s", expected.Username, user.Username) t.Fail() } if user.DisplayName != expected.DisplayName { t.Logf("Expected user DisplayName to be %s but was %s", expected.DisplayName, user.DisplayName) t.Fail() } if len(user.Groups) != len(expected.Groups) { t.Fatalf("Expected user Groups to be length %v but was %v", expected.Groups, user.Groups) } for i, group := range user.Groups { if group != expected.Groups[i] { t.Logf("Expected user group to be %s but was %s", expected.Groups[i], group) t.Fail() } } if len(user.Roles) != len(expected.Roles) { t.Fatalf("Expected user Roles to be length %v but was %v", expected.Roles, user.Roles) } for i, role := range user.Roles { if role != expected.Roles[i] { t.Logf("Expected user role to be %s but was %s", expected.Roles[i], role) t.Fail() } } } func assertUserAndMetadata(t *testing.T, user *UserAndMetadata, expected *UserAndMetadata) { if user.User.Username != expected.User.Username { t.Fatalf("Expected user Username to be %s but was %s", expected.User.Username, user.User.Username) } if len(user.User.Groups) != len(expected.User.Groups) { t.Fatalf("Expected user Groups to be length %v but was %v", expected.User.Groups, user.User.Groups) } for i, group := range user.User.Groups { if group != expected.User.Groups[i] { t.Fatalf("Expected user Groups 0 to be %s but was %s", expected.User.Groups[i], group) } } if len(user.User.Roles) != len(expected.User.Roles) { t.Fatalf("Expected user Roles to be length %v but was %v", expected.User.Roles, user.User.Roles) } if user.User.DisplayName != expected.User.DisplayName { t.Fatalf("Expected user DisplayName to be %s but was %s", expected.User.DisplayName, user.User.DisplayName) } if user.Domain != expected.Domain { t.Fatalf("Expected user Domain to be %s but was %s", expected.Domain, user.Domain) } if len(user.EffectiveRoles) != len(expected.EffectiveRoles) { t.Fatalf("Expected user EffectiveRoles to be length %v but was %v", expected.EffectiveRoles, user.EffectiveRoles) } } func (suite *UnitTestSuite) TestUserManagerGetUserDoesntExist() { retErr := `Unknown user.` resp := &mgmtResponse{ StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } username := "larry" mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/settings/rbac/users/local/"+username, req.Path) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("GET", req.Method) }). Return(resp, nil) usrMgr := &UserManager{ provider: mockProvider, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } _, err := usrMgr.GetUser(username, &GetUserOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrUserNotFound) { suite.T().Fatalf("Expected user not found error, %s", err) } } func (suite *UnitTestSuite) TestUserManagerDropUserDoesntExist() { retErr := `User was not found.` resp := &mgmtResponse{ StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } username := "larry" mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/settings/rbac/users/local/"+username, req.Path) suite.Assert().False(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("DELETE", req.Method) }). Return(resp, nil) usrMgr := &UserManager{ provider: mockProvider, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } err := usrMgr.DropUser(username, &DropUserOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrUserNotFound) { suite.T().Fatalf("Expected user not found error, %s", err) } } func (suite *UnitTestSuite) TestUserManagerGetGroupDoesntExist() { retErr := `Unknown group.` resp := &mgmtResponse{ StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } name := "g" mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/settings/rbac/groups/"+name, req.Path) suite.Assert().True(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("GET", req.Method) }). Return(resp, nil) usrMgr := &UserManager{ provider: mockProvider, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } _, err := usrMgr.GetGroup(name, &GetGroupOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrGroupNotFound) { suite.T().Fatalf("Expected user not found error, %s", err) } } func (suite *UnitTestSuite) TestUserManagerDropGroupDoesntExist() { retErr := `Group was not found.` resp := &mgmtResponse{ StatusCode: 404, Body: ioutil.NopCloser(bytes.NewReader([]byte(retErr))), } name := "g" mockProvider := new(mockMgmtProvider) mockProvider. On("executeMgmtRequest", nil, mock.AnythingOfType("mgmtRequest")). Run(func(args mock.Arguments) { req := args.Get(1).(mgmtRequest) suite.Assert().Equal("/settings/rbac/groups/"+name, req.Path) suite.Assert().False(req.IsIdempotent) suite.Assert().Equal(1*time.Second, req.Timeout) suite.Assert().Equal("DELETE", req.Method) }). Return(resp, nil) usrMgr := &UserManager{ provider: mockProvider, tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, } err := usrMgr.DropGroup(name, &DropGroupOptions{ Timeout: 1 * time.Second, }) if !errors.Is(err, ErrGroupNotFound) { suite.T().Fatalf("Expected user not found error, %s", err) } } gocb-2.6.3/collection.go000066400000000000000000000054671441755043100151360ustar00rootroot00000000000000package gocb // Collection represents a single collection. type Collection struct { collectionName string scope string bucket *Bucket timeoutsConfig TimeoutsConfig transcoder Transcoder retryStrategyWrapper *retryStrategyWrapper tracer RequestTracer meter *meterWrapper useMutationTokens bool getKvProvider func() (kvProvider, error) } func newCollection(scope *Scope, collectionName string) *Collection { return &Collection{ collectionName: collectionName, scope: scope.Name(), bucket: scope.bucket, timeoutsConfig: scope.timeoutsConfig, transcoder: scope.transcoder, retryStrategyWrapper: scope.retryStrategyWrapper, tracer: scope.tracer, meter: scope.meter, useMutationTokens: scope.useMutationTokens, getKvProvider: scope.getKvProvider, } } func (c *Collection) name() string { return c.collectionName } // ScopeName returns the name of the scope to which this collection belongs. func (c *Collection) ScopeName() string { return c.scope } // Bucket returns the bucket to which this collection belongs. // UNCOMMITTED: This API may change in the future. func (c *Collection) Bucket() *Bucket { return c.bucket } // Name returns the name of the collection. func (c *Collection) Name() string { return c.collectionName } // QueryIndexes returns a CollectionQueryIndexManager for managing query indexes. // UNCOMMITTED: This API may change in the future. func (c *Collection) QueryIndexes() *CollectionQueryIndexManager { // Ensure scope and collection names are populated, if the DefaultX functions on bucket are // used then the names will be empty by default. scopeName := c.scope if scopeName == "" { scopeName = "_default" } collectionName := c.collectionName if collectionName == "" { collectionName = "_default" } return &CollectionQueryIndexManager{ base: &baseQueryIndexManager{ provider: c.Bucket().Scope(scopeName), globalTimeout: c.timeoutsConfig.ManagementTimeout, tracer: c.tracer, meter: c.meter, }, bucketName: c.bucketName(), scopeName: scopeName, collectionName: collectionName, } } func (c *Collection) startKvOpTrace(operationName string, tracectx RequestSpanContext, noAttributes bool) RequestSpan { span := c.tracer.RequestSpan(tracectx, operationName) if !noAttributes { span.SetAttribute(spanAttribDBNameKey, c.bucket.Name()) span.SetAttribute(spanAttribDBCollectionNameKey, c.Name()) span.SetAttribute(spanAttribDBScopeNameKey, c.ScopeName()) span.SetAttribute(spanAttribServiceKey, "kv") span.SetAttribute(spanAttribOperationKey, operationName) } span.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) return span } func (c *Collection) bucketName() string { return c.bucket.Name() } gocb-2.6.3/collection_binary_crud.go000066400000000000000000000264221441755043100175110ustar00rootroot00000000000000package gocb import ( "context" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) // BinaryCollection is a set of binary operations. type BinaryCollection struct { collection *Collection } // AppendOptions are the options available to the Append operation. type AppendOptions struct { Timeout time.Duration DurabilityLevel DurabilityLevel PersistTo uint ReplicateTo uint Cas Cas RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } func (c *Collection) binaryAppend(id string, val []byte, opts *AppendOptions) (mutOut *MutationResult, errOut error) { if opts == nil { opts = &AppendOptions{} } opm := c.newKvOpManager("append", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) if err := opm.CheckReadyForOp(); err != nil { return nil, err } agent, err := c.getKvProvider() if err != nil { return nil, err } err = opm.Wait(agent.Append(gocbcore.AdjoinOptions{ Key: opm.DocumentID(), Value: val, CollectionName: opm.CollectionName(), ScopeName: opm.ScopeName(), DurabilityLevel: opm.DurabilityLevel(), DurabilityLevelTimeout: opm.DurabilityTimeout(), Cas: gocbcore.Cas(opts.Cas), RetryStrategy: opm.RetryStrategy(), TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, func(res *gocbcore.AdjoinResult, 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 } // Append appends a byte value to a document. func (c *BinaryCollection) Append(id string, val []byte, opts *AppendOptions) (mutOut *MutationResult, errOut error) { return c.collection.binaryAppend(id, val, opts) } // PrependOptions are the options available to the Prepend operation. type PrependOptions struct { Timeout time.Duration DurabilityLevel DurabilityLevel PersistTo uint ReplicateTo uint Cas Cas RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } func (c *Collection) binaryPrepend(id string, val []byte, opts *PrependOptions) (mutOut *MutationResult, errOut error) { if opts == nil { opts = &PrependOptions{} } opm := c.newKvOpManager("prepend", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) if err := opm.CheckReadyForOp(); err != nil { return nil, err } agent, err := c.getKvProvider() if err != nil { return nil, err } err = opm.Wait(agent.Prepend(gocbcore.AdjoinOptions{ Key: opm.DocumentID(), Value: val, CollectionName: opm.CollectionName(), ScopeName: opm.ScopeName(), DurabilityLevel: opm.DurabilityLevel(), DurabilityLevelTimeout: opm.DurabilityTimeout(), Cas: gocbcore.Cas(opts.Cas), RetryStrategy: opm.RetryStrategy(), TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, func(res *gocbcore.AdjoinResult, 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 } // Prepend prepends a byte value to a document. func (c *BinaryCollection) Prepend(id string, val []byte, opts *PrependOptions) (mutOut *MutationResult, errOut error) { return c.collection.binaryPrepend(id, val, opts) } // IncrementOptions are the options available to the Increment operation. type IncrementOptions struct { Timeout time.Duration // Expiry is the length of time that the document will be stored in Couchbase. // A value of 0 will set the document to never expire. Expiry time.Duration // Initial, if non-negative, is the `initial` value to use for the document if it does not exist. // If present, this is the value that will be returned by a successful operation. Initial int64 // Delta is the value to use for incrementing/decrementing if Initial is not present. Delta uint64 DurabilityLevel DurabilityLevel PersistTo uint ReplicateTo uint RetryStrategy RetryStrategy ParentSpan RequestSpan // Deprecated: Cas is not supported by the server for Increment, and is no longer used. Cas Cas // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } func (c *Collection) binaryIncrement(id string, opts *IncrementOptions) (countOut *CounterResult, errOut error) { if opts == nil { opts = &IncrementOptions{} } if opts.Cas > 0 { return nil, makeInvalidArgumentsError("cas is not supported by the server for the Increment operation") } opm := c.newKvOpManager("increment", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) realInitial := uint64(0xFFFFFFFFFFFFFFFF) if opts.Initial >= 0 { realInitial = uint64(opts.Initial) } if err := opm.CheckReadyForOp(); err != nil { return nil, err } agent, err := c.getKvProvider() if err != nil { return nil, err } err = opm.Wait(agent.Increment(gocbcore.CounterOptions{ Key: opm.DocumentID(), Delta: opts.Delta, Initial: realInitial, Expiry: durationToExpiry(opts.Expiry), CollectionName: opm.CollectionName(), ScopeName: opm.ScopeName(), DurabilityLevel: opm.DurabilityLevel(), DurabilityLevelTimeout: opm.DurabilityTimeout(), RetryStrategy: opm.RetryStrategy(), TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, func(res *gocbcore.CounterResult, err error) { if err != nil { errOut = opm.EnhanceErr(err) opm.Reject() return } countOut = &CounterResult{} countOut.cas = Cas(res.Cas) countOut.mt = opm.EnhanceMt(res.MutationToken) countOut.content = res.Value opm.Resolve(countOut.mt) })) if err != nil { errOut = err } return } // Increment performs an atomic addition for an integer document. Passing a // non-negative `initial` value will cause the document to be created if it did not // already exist. func (c *BinaryCollection) Increment(id string, opts *IncrementOptions) (countOut *CounterResult, errOut error) { return c.collection.binaryIncrement(id, opts) } // DecrementOptions are the options available to the Decrement operation. type DecrementOptions struct { Timeout time.Duration // Expiry is the length of time that the document will be stored in Couchbase. // A value of 0 will set the document to never expire. Expiry time.Duration // Initial, if non-negative, is the `initial` value to use for the document if it does not exist. // If present, this is the value that will be returned by a successful operation. Initial int64 // Delta is the value to use for incrementing/decrementing if Initial is not present. Delta uint64 DurabilityLevel DurabilityLevel PersistTo uint ReplicateTo uint RetryStrategy RetryStrategy ParentSpan RequestSpan // Deprecated: Cas is not supported by the server for Decrement, and is no longer used. Cas Cas // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } func (c *Collection) binaryDecrement(id string, opts *DecrementOptions) (countOut *CounterResult, errOut error) { if opts == nil { opts = &DecrementOptions{} } if opts.Cas > 0 { return nil, makeInvalidArgumentsError("cas is not supported by the server for the Decrement operation") } opm := c.newKvOpManager("decrement", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) realInitial := uint64(0xFFFFFFFFFFFFFFFF) if opts.Initial >= 0 { realInitial = uint64(opts.Initial) } if err := opm.CheckReadyForOp(); err != nil { return nil, err } agent, err := c.getKvProvider() if err != nil { return nil, err } err = opm.Wait(agent.Decrement(gocbcore.CounterOptions{ Key: opm.DocumentID(), Delta: opts.Delta, Initial: realInitial, Expiry: durationToExpiry(opts.Expiry), CollectionName: opm.CollectionName(), ScopeName: opm.ScopeName(), DurabilityLevel: opm.DurabilityLevel(), DurabilityLevelTimeout: opm.DurabilityTimeout(), RetryStrategy: opm.RetryStrategy(), TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, func(res *gocbcore.CounterResult, err error) { if err != nil { errOut = opm.EnhanceErr(err) opm.Reject() return } countOut = &CounterResult{} countOut.cas = Cas(res.Cas) countOut.mt = opm.EnhanceMt(res.MutationToken) countOut.content = res.Value opm.Resolve(countOut.mt) })) if err != nil { errOut = err } return } // Decrement performs an atomic subtraction for an integer document. Passing a // non-negative `initial` value will cause the document to be created if it did not // already exist. func (c *BinaryCollection) Decrement(id string, opts *DecrementOptions) (countOut *CounterResult, errOut error) { return c.collection.binaryDecrement(id, opts) } gocb-2.6.3/collection_binary_crud_test.go000066400000000000000000000200621441755043100205420ustar00rootroot00000000000000package gocb import ( "time" "github.com/couchbase/gocbcore/v10/memd" ) func (suite *IntegrationTestSuite) TestBinaryAppend() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(AdjoinFeature) colBinary := globalCollection.Binary() docId := generateDocId("binaryAppend") tcoder := NewRawBinaryTranscoder() res, err := globalCollection.Upsert(docId, []byte("foo"), &UpsertOptions{ Transcoder: tcoder, Timeout: 30 * time.Second, }) if err != nil { suite.T().Fatalf("Failed to Upsert, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } appendRes, err := colBinary.Append(docId, []byte("bar"), nil) if err != nil { suite.T().Fatalf("Failed to Append, err: %v", err) } if appendRes.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } appendDoc, err := globalCollection.Get(docId, &GetOptions{ Transcoder: tcoder, }) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var appendContent []byte err = appendDoc.Content(&appendContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if string(appendContent) != "foobar" { suite.T().Fatalf("Expected append result to be foobar but was %s", appendContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "append", memd.CmdAppend.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "append", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestBinaryPrepend() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(AdjoinFeature) colBinary := globalCollection.Binary() docId := generateDocId("binaryPrepend") tcoder := NewRawBinaryTranscoder() res, err := globalCollection.Upsert(docId, []byte("foo"), &UpsertOptions{ Transcoder: tcoder, }) if err != nil { suite.T().Fatalf("Failed to Upsert, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } appendRes, err := colBinary.Prepend(docId, []byte("bar"), nil) if err != nil { suite.T().Fatalf("Failed to Append, err: %v", err) } if appendRes.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } appendDoc, err := globalCollection.Get(docId, &GetOptions{ Transcoder: tcoder, }) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var appendContent []byte err = appendDoc.Content(&appendContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if string(appendContent) != "barfoo" { suite.T().Fatalf("Expected prepend result to be barfoo but was %s", appendContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "prepend", memd.CmdPrepend.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "prepend", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestBinaryIncrement() { suite.skipIfUnsupported(KeyValueFeature) docId := generateDocId("binaryIncrement") colBinary := globalCollection.Binary() res, err := colBinary.Increment(docId, &IncrementOptions{ Delta: 10, }) if err != nil { suite.T().Fatalf("Failed to Increment, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } if res.Content() != 0 { suite.T().Fatalf("Expected counter value to be 0 but was %d", res.Content()) } res, err = colBinary.Increment(docId, &IncrementOptions{ Delta: 10, }) if err != nil { suite.T().Fatalf("Failed to Increment, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } if res.Content() != 10 { suite.T().Fatalf("Expected counter value to be 10 but was %d", res.Content()) } res, err = colBinary.Increment(docId, &IncrementOptions{ Delta: 10, }) if err != nil { suite.T().Fatalf("Failed to Increment, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } if res.Content() != 20 { suite.T().Fatalf("Expected counter value to be 20 but was %d", res.Content()) } incrementDoc, err := globalCollection.Get(docId, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var incrementContent int err = incrementDoc.Content(&incrementContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if incrementContent != 20 { suite.T().Fatalf("Expected counter value to be 20 but was %d", res.Content()) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 4) suite.AssertKvOpSpan(nilParents[0], "increment", memd.CmdIncrement.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "increment", memd.CmdIncrement.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "increment", memd.CmdIncrement.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "increment", 3, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestBinaryDecrement() { suite.skipIfUnsupported(KeyValueFeature) docId := generateDocId("binaryDecrement") colBinary := globalCollection.Binary() res, err := colBinary.Decrement(docId, &DecrementOptions{ Delta: 10, Initial: 100, }) if err != nil { suite.T().Fatalf("Failed to Decrement, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } if res.Content() != 100 { suite.T().Fatalf("Expected counter value to be 100 but was %d", res.Content()) } res, err = colBinary.Decrement(docId, &DecrementOptions{ Delta: 10, }) if err != nil { suite.T().Fatalf("Failed to Decrement, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } if res.Content() != 90 { suite.T().Fatalf("Expected counter value to be 90 but was %d", res.Content()) } res, err = colBinary.Decrement(docId, &DecrementOptions{ Delta: 10, }) if err != nil { suite.T().Fatalf("Failed to Decrement, err: %v", err) } if res.Cas() == 0 { suite.T().Fatalf("Expected Cas to be non-zero") } if res.Content() != 80 { suite.T().Fatalf("Expected counter value to be 80 but was %d", res.Content()) } incrementDoc, err := globalCollection.Get(docId, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var incrementContent int err = incrementDoc.Content(&incrementContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if incrementContent != 80 { suite.T().Fatalf("Expected counter value to be 80 but was %d", res.Content()) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 4) suite.AssertKvOpSpan(nilParents[0], "decrement", memd.CmdDecrement.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "decrement", memd.CmdDecrement.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "decrement", memd.CmdDecrement.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "decrement", 3, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } gocb-2.6.3/collection_bulk.go000066400000000000000000000511101441755043100161350ustar00rootroot00000000000000// nolint: unused package gocb import ( "context" "time" "github.com/couchbase/gocbcore/v10" ) type bulkOp struct { pendop gocbcore.PendingOp finishFn func() } func (op *bulkOp) cancel() { op.pendop.Cancel() } func (op *bulkOp) finish() { op.finishFn() } // BulkOp represents a single operation that can be submitted (within a list of more operations) to .Do() // You can create a bulk operation by instantiating one of the implementations of BulkOp, // such as GetOp, UpsertOp, ReplaceOp, and more. // UNCOMMITTED: This API may change in the future. type BulkOp interface { execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) markError(err error) cancel() finish() } // BulkOpOptions are the set of options available when performing BulkOps using Do. type BulkOpOptions struct { Timeout time.Duration Transcoder Transcoder RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context } // Do execute one or more `BulkOp` items in parallel. // UNCOMMITTED: This API may change in the future. func (c *Collection) Do(ops []BulkOp, opts *BulkOpOptions) error { if opts == nil { opts = &BulkOpOptions{} } var tracectx RequestSpanContext if opts.ParentSpan != nil { tracectx = opts.ParentSpan.Context() } span := c.startKvOpTrace("bulk", tracectx, false) defer span.End() timeout := opts.Timeout if opts.Timeout == 0 { timeout = c.timeoutsConfig.KVTimeout * time.Duration(len(ops)) } retryWrapper := c.retryStrategyWrapper if opts.RetryStrategy != nil { retryWrapper = newRetryStrategyWrapper(opts.RetryStrategy) } if opts.Transcoder == nil { opts.Transcoder = c.transcoder } agent, err := c.getKvProvider() if err != nil { return err } // Make the channel big enough to hold all our ops in case // we get delayed inside execute (don't want to block the // individual op handlers when they dispatch their signal). signal := make(chan BulkOp, len(ops)) for _, item := range ops { item.execute(span.Context(), c, agent, opts.Transcoder, signal, retryWrapper, time.Now().Add(timeout), c.startKvOpTrace) } for range ops { item := <-signal // We're really just clearing the pendop from this thread, // since it already completed, no cancel actually occurs item.finish() } return nil } // GetOp represents a type of `BulkOp` used for Get operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type GetOp struct { bulkOp ID string Result *GetResult Err error } func (item *GetOp) markError(err error) { item.Err = err } func (item *GetOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("get", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "get", start) } op, err := provider.Get(gocbcore.GetOptions{ Key: []byte(item.ID), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.GetResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &GetResult{ Result: Result{ cas: Cas(res.Cas), }, transcoder: transcoder, contents: res.Value, flags: res.Flags, } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // GetAndTouchOp represents a type of `BulkOp` used for GetAndTouch operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type GetAndTouchOp struct { bulkOp ID string Expiry time.Duration Result *GetResult Err error } func (item *GetAndTouchOp) markError(err error) { item.Err = err } func (item *GetAndTouchOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("get_and_touch", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "get_and_touch", start) } op, err := provider.GetAndTouch(gocbcore.GetAndTouchOptions{ Key: []byte(item.ID), Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.GetAndTouchResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &GetResult{ Result: Result{ cas: Cas(res.Cas), }, transcoder: transcoder, contents: res.Value, flags: res.Flags, } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // TouchOp represents a type of `BulkOp` used for Touch operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type TouchOp struct { bulkOp ID string Expiry time.Duration Result *MutationResult Err error } func (item *TouchOp) markError(err error) { item.Err = err } func (item *TouchOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("touch", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "touch", start) } op, err := provider.Touch(gocbcore.TouchOptions{ Key: []byte(item.ID), Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.TouchResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // RemoveOp represents a type of `BulkOp` used for Remove operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type RemoveOp struct { bulkOp ID string Cas Cas Result *MutationResult Err error } func (item *RemoveOp) markError(err error) { item.Err = err } func (item *RemoveOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("remove", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "remove", start) } op, err := provider.Delete(gocbcore.DeleteOptions{ Key: []byte(item.ID), Cas: gocbcore.Cas(item.Cas), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.DeleteResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // UpsertOp represents a type of `BulkOp` used for Upsert operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type UpsertOp struct { bulkOp ID string Value interface{} Expiry time.Duration Cas Cas Result *MutationResult Err error } func (item *UpsertOp) markError(err error) { item.Err = err } func (item *UpsertOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("upsert", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "upsert", start) } etrace := c.startKvOpTrace("request_encoding", span.Context(), true) bytes, flags, err := transcoder.Encode(item.Value) etrace.End() if err != nil { item.Err = err signal <- item return } op, err := provider.Set(gocbcore.SetOptions{ Key: []byte(item.ID), Value: bytes, Flags: flags, Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.StoreResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // InsertOp represents a type of `BulkOp` used for Insert operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type InsertOp struct { bulkOp ID string Value interface{} Expiry time.Duration Result *MutationResult Err error } func (item *InsertOp) markError(err error) { item.Err = err } func (item *InsertOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("insert", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "insert", start) } etrace := c.startKvOpTrace("request_encoding", span.Context(), true) bytes, flags, err := transcoder.Encode(item.Value) if err != nil { etrace.End() item.Err = err signal <- item return } etrace.End() op, err := provider.Add(gocbcore.AddOptions{ Key: []byte(item.ID), Value: bytes, Flags: flags, Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.StoreResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // ReplaceOp represents a type of `BulkOp` used for Replace operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type ReplaceOp struct { bulkOp ID string Value interface{} Expiry time.Duration Cas Cas Result *MutationResult Err error } func (item *ReplaceOp) markError(err error) { item.Err = err } func (item *ReplaceOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("replace", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "replace", start) } etrace := c.startKvOpTrace("request_encoding", span.Context(), true) bytes, flags, err := transcoder.Encode(item.Value) if err != nil { etrace.End() item.Err = err signal <- item return } etrace.End() op, err := provider.Replace(gocbcore.ReplaceOptions{ Key: []byte(item.ID), Value: bytes, Flags: flags, Cas: gocbcore.Cas(item.Cas), Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.StoreResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // AppendOp represents a type of `BulkOp` used for Append operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type AppendOp struct { bulkOp ID string Value string Result *MutationResult Err error } func (item *AppendOp) markError(err error) { item.Err = err } func (item *AppendOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("append", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "append", start) } op, err := provider.Append(gocbcore.AdjoinOptions{ Key: []byte(item.ID), Value: []byte(item.Value), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.AdjoinResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // PrependOp represents a type of `BulkOp` used for Prepend operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type PrependOp struct { bulkOp ID string Value string Result *MutationResult Err error } func (item *PrependOp) markError(err error) { item.Err = err } func (item *PrependOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("prepend", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "prepend", start) } op, err := provider.Prepend(gocbcore.AdjoinOptions{ Key: []byte(item.ID), Value: []byte(item.Value), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.AdjoinResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &MutationResult{ Result: Result{ cas: Cas(res.Cas), }, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // IncrementOp represents a type of `BulkOp` used for Increment operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type IncrementOp struct { bulkOp ID string Delta int64 Initial int64 Expiry time.Duration Result *CounterResult Err error } func (item *IncrementOp) markError(err error) { item.Err = err } func (item *IncrementOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("increment", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "increment", start) } realInitial := uint64(0xFFFFFFFFFFFFFFFF) if item.Initial > 0 { realInitial = uint64(item.Initial) } op, err := provider.Increment(gocbcore.CounterOptions{ Key: []byte(item.ID), Delta: uint64(item.Delta), Initial: realInitial, Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.CounterResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &CounterResult{ MutationResult: MutationResult{ Result: Result{ cas: Cas(res.Cas), }, }, content: res.Value, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } // DecrementOp represents a type of `BulkOp` used for Decrement operations. See BulkOp. // UNCOMMITTED: This API may change in the future. type DecrementOp struct { bulkOp ID string Delta int64 Initial int64 Expiry time.Duration Result *CounterResult Err error } func (item *DecrementOp) markError(err error) { item.Err = err } func (item *DecrementOp) execute(tracectx RequestSpanContext, c *Collection, provider kvProvider, transcoder Transcoder, signal chan BulkOp, retryWrapper *retryStrategyWrapper, deadline time.Time, startSpanFunc func(string, RequestSpanContext, bool) RequestSpan) { span := startSpanFunc("decrement", tracectx, false) start := time.Now() item.bulkOp.finishFn = func() { span.End() c.meter.ValueRecord(meterValueServiceKV, "decrement", start) } realInitial := uint64(0xFFFFFFFFFFFFFFFF) if item.Initial > 0 { realInitial = uint64(item.Initial) } op, err := provider.Decrement(gocbcore.CounterOptions{ Key: []byte(item.ID), Delta: uint64(item.Delta), Initial: realInitial, Expiry: durationToExpiry(item.Expiry), CollectionName: c.name(), ScopeName: c.ScopeName(), RetryStrategy: retryWrapper, TraceContext: span.Context(), Deadline: deadline, }, func(res *gocbcore.CounterResult, err error) { item.Err = maybeEnhanceCollKVErr(err, provider, c, item.ID) if item.Err == nil { item.Result = &CounterResult{ MutationResult: MutationResult{ Result: Result{ cas: Cas(res.Cas), }, }, content: res.Value, } if res.MutationToken.VbUUID != 0 { mutTok := &MutationToken{ token: res.MutationToken, bucketName: c.bucketName(), } item.Result.mt = mutTok } } signal <- item }) if err != nil { item.Err = err signal <- item } else { item.bulkOp.pendop = op } } gocb-2.6.3/collection_bulk_test.go000066400000000000000000000172771441755043100172140ustar00rootroot00000000000000package gocb import ( "fmt" "time" "github.com/couchbase/gocbcore/v10/memd" ) func (suite *IntegrationTestSuite) TestUpsertGetBulk() { suite.skipIfUnsupported(KeyValueFeature) var ops []BulkOp for i := 0; i < 20; i++ { ops = append(ops, &UpsertOp{ ID: fmt.Sprintf("%d", i), Value: "test", Expiry: 20 * time.Second, }) } err := globalCollection.Do(ops, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for upserts %v", err) } for _, op := range ops { upsertOp, ok := op.(*UpsertOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into UpsertOp") } if upsertOp.Err != nil { suite.T().Fatalf("Expected UpsertOp Err to be nil but was %v", upsertOp.Err) } if upsertOp.Result.Cas() == 0 { suite.T().Fatalf("Expected UpsertOp Cas to be non zero") } } var getOps []BulkOp for i := 0; i < 20; i++ { getOps = append(getOps, &GetOp{ ID: fmt.Sprintf("%d", i), }) } err = globalCollection.Do(getOps, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for gets %v", err) } for _, op := range getOps { getOp, ok := op.(*GetOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into GetOp") } if getOp.Err != nil { suite.T().Fatalf("Expected GetOp Err to be nil but was %v", getOp.Err) } if getOp.Result.Cas() == 0 { suite.T().Fatalf("Expected GetOp Cas to be non zero") } var val string err = getOp.Result.Content(&val) if err != nil { suite.T().Fatalf("Failed to get content from GetOp %v", err) } if val != "test" { suite.T().Fatalf("Expected GetOp value to be test but was %s", val) } } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvSpan(nilParents[0], "bulk", DurabilityLevelNone) suite.AssertKvSpan(nilParents[1], "bulk", DurabilityLevelNone) suite.Require().Len(nilParents[0].Spans["upsert"], 20) suite.Require().Len(nilParents[1].Spans["get"], 20) for _, span := range nilParents[0].Spans["upsert"] { suite.AssertKvOpSpan(span, "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) } for _, span := range nilParents[1].Spans["get"] { suite.AssertKvOpSpan(span, "get", memd.CmdGet.Name(), false, DurabilityLevelNone) } suite.AssertKVMetrics(meterNameCBOperations, "upsert", 20, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 20, false) } func (suite *IntegrationTestSuite) TestInsertDocsBulk() { suite.skipIfUnsupported(KeyValueFeature) var ops []BulkOp for i := 0; i < 20; i++ { ops = append(ops, &InsertOp{ ID: fmt.Sprintf("insert-docs-bulk-%d-%s", i, generateDocId("")), Value: "test", Expiry: 20 * time.Second, }) } err := globalCollection.Do(ops, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for inserts %v", err) } for _, op := range ops { insertOp, ok := op.(*InsertOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into InsertOp") } if insertOp.Err != nil { suite.T().Fatalf("Expected UpsertOp Err to be nil but was %v", insertOp.Err) } } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.AssertKvSpan(nilParents[0], "bulk", DurabilityLevelNone) suite.Require().Len(nilParents[0].Spans["insert"], 20) for _, span := range nilParents[0].Spans["insert"] { suite.AssertKvOpSpan(span, "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) } suite.AssertKVMetrics(meterNameCBOperations, "insert", 20, false) } func (suite *IntegrationTestSuite) TestReplaceOperationBulk() { suite.skipIfUnsupported(KeyValueFeature) var ops []BulkOp for i := 0; i < 20; i++ { ops = append(ops, &UpsertOp{ ID: fmt.Sprintf("replace-docs-bulk-%d", i), Value: "test", Expiry: 20 * time.Second, }) } err := globalCollection.Do(ops, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for upserts %v", err) } for _, op := range ops { upsertOp, ok := op.(*UpsertOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into UpsertOp") } if upsertOp.Err != nil { suite.T().Fatalf("Expected UpsertOp Err to be nil but was %v", upsertOp.Err) } } var replaceOps []BulkOp for i := 0; i < 20; i++ { replaceOps = append(replaceOps, &ReplaceOp{ ID: fmt.Sprintf("replace-docs-bulk-%d", i), }) } err = globalCollection.Do(replaceOps, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for replace %v", err) } for _, op := range replaceOps { replaceOp, ok := op.(*ReplaceOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into ReplaceOp") } if replaceOp.Err != nil { suite.T().Fatalf("Expected UpsertOp Err to be nil but was %v", replaceOp.Err) } } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvSpan(nilParents[0], "bulk", DurabilityLevelNone) suite.AssertKvSpan(nilParents[1], "bulk", DurabilityLevelNone) suite.Require().Len(nilParents[0].Spans["upsert"], 20) suite.Require().Len(nilParents[1].Spans["replace"], 20) for _, span := range nilParents[0].Spans["upsert"] { suite.AssertKvOpSpan(span, "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) } for _, span := range nilParents[1].Spans["replace"] { suite.AssertKvOpSpan(span, "replace", memd.CmdReplace.Name(), true, DurabilityLevelNone) } suite.AssertKVMetrics(meterNameCBOperations, "upsert", 20, false) suite.AssertKVMetrics(meterNameCBOperations, "replace", 20, false) } func (suite *IntegrationTestSuite) TestRemoveOperationBulk() { suite.skipIfUnsupported(KeyValueFeature) var ops []BulkOp for i := 0; i < 20; i++ { ops = append(ops, &UpsertOp{ ID: fmt.Sprintf("remove-docs-bulk-%d", i), Value: "test", Expiry: 20 * time.Second, }) } err := globalCollection.Do(ops, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for upserts %v", err) } for _, op := range ops { upsertOp, ok := op.(*UpsertOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into UpsertOp") } if upsertOp.Err != nil { suite.T().Fatalf("Expected UpsertOp Err to be nil but was %v", upsertOp.Err) } if upsertOp.Result.Cas() == 0 { suite.T().Fatalf("Expected UpsertOp Cas to be non zero") } } var removeOps []BulkOp for i := 0; i < 20; i++ { removeOps = append(removeOps, &RemoveOp{ ID: fmt.Sprintf("remove-docs-bulk-%d", i), }) } err = globalCollection.Do(removeOps, nil) if err != nil { suite.T().Fatalf("Expected Do to not error for removeops %v", err) } for _, op := range removeOps { removeOp, ok := op.(*RemoveOp) if !ok { suite.T().Fatalf("Could not type assert BulkOp into RemoveOp") } if removeOp.Err != nil { suite.T().Fatalf("Expected RemoveOp Err to be nil but was %v", removeOp.Err) } if removeOp.Result.Cas() == 0 { suite.T().Fatalf("Expected RemoveOp Cas to be non zero") } } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvSpan(nilParents[0], "bulk", DurabilityLevelNone) suite.AssertKvSpan(nilParents[1], "bulk", DurabilityLevelNone) suite.Require().Len(nilParents[0].Spans["upsert"], 20) suite.Require().Len(nilParents[1].Spans["remove"], 20) for _, span := range nilParents[0].Spans["upsert"] { suite.AssertKvOpSpan(span, "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) } for _, span := range nilParents[1].Spans["remove"] { suite.AssertKvOpSpan(span, "remove", memd.CmdDelete.Name(), false, DurabilityLevelNone) } suite.AssertKVMetrics(meterNameCBOperations, "upsert", 20, false) suite.AssertKVMetrics(meterNameCBOperations, "remove", 20, false) } gocb-2.6.3/collection_crud.go000066400000000000000000001100221441755043100161330ustar00rootroot00000000000000package gocb import ( "context" "errors" "sync" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) 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) WaitForConfigSnapshot(deadline time.Time, opts gocbcore.WaitForConfigSnapshotOptions, cb gocbcore.WaitForConfigSnapshotCallback) (gocbcore.PendingOp, error) RangeScanCreate(vbID uint16, opts gocbcore.RangeScanCreateOptions, cb gocbcore.RangeScanCreateCallback) (gocbcore.PendingOp, error) RangeScanContinue(scanUUID []byte, vbID uint16, opts gocbcore.RangeScanContinueOptions, dataCb gocbcore.RangeScanContinueDataCallback, actionCb gocbcore.RangeScanContinueActionCallback) (gocbcore.PendingOp, error) RangeScanCancel(scanUUID []byte, vbID uint16, opts gocbcore.RangeScanCancelOptions, cb gocbcore.RangeScanCancelCallback) (gocbcore.PendingOp, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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", opts.ParentSpan) defer opm.Finish(false) 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) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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 ParentSpan RequestSpan PreserveExpiry bool // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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", opts.ParentSpan) defer opm.Finish(false) 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) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) opm.SetPreserveExpiry(opts.PreserveExpiry) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), PreserveExpiry: opm.PreserveExpiry(), }, 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 ParentSpan RequestSpan PreserveExpiry bool // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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{} } if opts.Expiry > 0 && opts.PreserveExpiry { return nil, makeInvalidArgumentsError("cannot use expiry and preserve ttl together for replace") } opm := c.newKvOpManager("replace", opts.ParentSpan) defer opm.Finish(false) 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) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) opm.SetPreserveExpiry(opts.PreserveExpiry) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), PreserveExpiry: opm.PreserveExpiry(), }, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetTranscoder(opts.Transcoder) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetTranscoder(opts.Transcoder) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) if err := opm.CheckReadyForOp(); err != nil { return nil, err } var withFlags bool numProjects := len(opts.Project) if opts.WithExpiry { if numProjects == 0 { // This must be a full get with expiry withFlags = true } 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 withFlags { // We also need to fetch the flags, we need them for transcoding and they aren't included in a lookupin // response. We only need these when doing a full get with expiry. ops = append(ops, GetSpec("$document.flags", &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.LookupIn(id, ops, &LookupInOptions{ ParentSpan: opm.TraceSpan(), noMetrics: true, Context: opts.Context, }) if err != nil { return nil, err } doc := &GetResult{} if opts.WithExpiry { // if expiration was requested then extract and remove it from the results var expires int64 err = result.ContentAt(0, &expires) if err != nil { return nil, err } expiryTime := time.Unix(expires, 0) doc.expiryTime = &expiryTime ops = ops[1:] result.contents = result.contents[1:] if withFlags { var flags uint32 err = result.ContentAt(0, &flags) if err != nil { return nil, err } doc.flags = flags 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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( ctx context.Context, span RequestSpan, id string, replicaIdx int, transcoder Transcoder, retryStrategy RetryStrategy, cancelCh chan struct{}, timeout time.Duration, user string, ) (docOut *GetReplicaResult, errOut error) { opm := c.newKvOpManager("get_replica", span) defer opm.Finish(true) opm.SetDocumentID(id) opm.SetTranscoder(transcoder) opm.SetRetryStrategy(retryStrategy) opm.SetTimeout(timeout) opm.SetCancelCh(cancelCh) opm.SetImpersonate(user) opm.SetContext(ctx) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } noMetrics bool } // GetAllReplicasResult represents the results of a GetAllReplicas operation. type GetAllReplicasResult struct { lock sync.Mutex totalRequests uint32 successResults uint32 totalResults uint32 resCh chan *GetReplicaResult cancelCh chan struct{} span RequestSpan childReqsCompleteCh chan struct{} valueRecorder ValueRecorder startedTime time.Time } func (r *GetAllReplicasResult) addFailed() { r.lock.Lock() r.totalResults++ if r.totalResults == r.totalRequests { close(r.childReqsCompleteCh) } r.lock.Unlock() } 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.successResults++ resultCount := r.successResults if resultCount <= r.totalRequests { r.resCh <- res } if resultCount == r.totalRequests { close(r.cancelCh) close(r.resCh) r.span.End() if r.valueRecorder != nil { r.valueRecorder.RecordValue(uint64(time.Since(r.startedTime).Microseconds())) } } r.totalResults++ if r.totalResults == r.totalRequests { close(r.childReqsCompleteCh) } 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.successResults r.successResults += 100000 // We only have to close everything if the addResult method didn't already // close them due to already having completed every request var weClosed bool if prevResultCount < r.totalRequests { close(r.cancelCh) close(r.resCh) weClosed = true } r.lock.Unlock() if weClosed { // We need to wait for the child requests spans to be completed. <-r.childReqsCompleteCh r.span.End() } 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{} } var tracectx RequestSpanContext if opts.ParentSpan != nil { tracectx = opts.ParentSpan.Context() } ctx := opts.Context if ctx == nil { ctx = context.Background() } span := c.startKvOpTrace("get_all_replicas", tracectx, false) // 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 := c.waitForConfigSnapshot(ctx, deadline, agent) 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{}) var recorder ValueRecorder if !opts.noMetrics { recorder, err = c.meter.ValueRecorder(meterValueServiceKV, "get_all_replicas") if err != nil { logDebugf("Failed to create value recorder: %v", err) } } repRes := &GetAllReplicasResult{ totalRequests: uint32(numServers), resCh: outCh, cancelCh: cancelCh, span: span, childReqsCompleteCh: make(chan struct{}), valueRecorder: recorder, startedTime: time.Now(), } // 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(context.Background(), span, id, replicaIdx, transcoder, retryStrategy, cancelCh, timeout, opts.Internal.User) if err != nil { repRes.addFailed() 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) } case <-cancelCh: // If the cancel channel closes, we are done case <-ctx.Done(): err := repRes.Close() if err != nil { logDebugf("failed to close GetAllReplicas response: %s", err) } } }() return repRes, nil } // GetAnyReplicaOptions are the options available to the GetAnyReplica command. type GetAnyReplicaOptions struct { Transcoder Transcoder Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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{} } start := time.Now() defer c.meter.ValueRecord("kv", "get_any_replica", start) var tracectx RequestSpanContext if opts.ParentSpan != nil { tracectx = opts.ParentSpan.Context() } span := c.startKvOpTrace("get_any_replica", tracectx, false) defer span.End() repRes, err := c.GetAllReplicas(id, &GetAllReplicaOptions{ Timeout: opts.Timeout, Transcoder: opts.Transcoder, RetryStrategy: opts.RetryStrategy, Internal: opts.Internal, ParentSpan: span, noMetrics: true, Context: opts.Context, }) 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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("get_and_touch", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetTranscoder(opts.Transcoder) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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("get_and_lock", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetTranscoder(opts.Transcoder) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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(false) opm.SetDocumentID(id) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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 ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } // 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(false) opm.SetDocumentID(id) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) 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.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, 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} } gocb-2.6.3/collection_crud_bench_test.go000066400000000000000000000107721441755043100203440ustar00rootroot00000000000000package gocb import ( "fmt" "sync/atomic" "testing" "time" ) type benchDoc struct { Data []byte `json:"data"` } func BenchmarkUpsert(b *testing.B) { b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} // We need to ensure that connections are up and ready before starting the benchmark err := globalBucket.WaitUntilReady(5*time.Second, &WaitUntilReadyOptions{ ServiceTypes: []ServiceType{ServiceTypeKeyValue}, }) if err != nil { b.Fatalf("Wait until ready failed: %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { var i uint32 for pb.Next() { keyNum := atomic.AddUint32(&i, 1) _, err := globalCollection.Upsert(fmt.Sprintf("upsert-%d", keyNum), doc, nil) if err != nil { b.Fatalf("failed to upsert %d: %v", keyNum, err) } atomic.AddUint32(&i, 1) } }) } func BenchmarkReplace(b *testing.B) { b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} _, err := globalCollection.Upsert("upsert-replace-1", doc, nil) if err != nil { b.Fatalf("failed to upsert %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := globalCollection.Replace("upsert-replace-1", doc, nil) if err != nil { b.Fatalf("failed to replace %v", err) } } }) } func BenchmarkGet(b *testing.B) { b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} _, err := globalCollection.Upsert("upsert-get-1", doc, nil) if err != nil { b.Fatalf("failed to upsert %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := globalCollection.Get("upsert-get-1", nil) if err != nil { b.Fatalf("failed to get %v", err) } } }) } func BenchmarkExists(b *testing.B) { if !globalCluster.SupportsFeature(XattrFeature) { b.Skip() } b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} _, err := globalCollection.Upsert("upsert-exists-1", doc, nil) if err != nil { b.Fatalf("failed to upsert %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := globalCollection.Exists("upsert-exists-1", nil) if err != nil { b.Fatalf("failed to exists %v", err) } } }) } func BenchmarkGetFromReplica(b *testing.B) { b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} _, err := globalCollection.Upsert("upsert-replica-1", doc, nil) if err != nil { b.Fatalf("failed to upsert %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := globalCollection.GetAnyReplica("upsert-replica-1", nil) if err != nil { b.Fatalf("failed to get replica %v", err) } } }) } func BenchmarkGetAndTouch(b *testing.B) { b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} _, err := globalCollection.Upsert("upsert-get-and-touch-1", doc, nil) if err != nil { b.Fatalf("failed to upsert %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := globalCollection.GetAndTouch("upsert-get-and-touch-1", 10, nil) if err != nil { b.Fatalf("failed to get and touch %v", err) } } }) } func BenchmarkTouch(b *testing.B) { b.ReportAllocs() // Generate 256 bytes of random data for the document randomBytes := make([]byte, 256) for i := 0; i < len(randomBytes); i++ { randomBytes[i] = byte(i) } doc := benchDoc{Data: randomBytes} _, err := globalCollection.Upsert("upsert-touch-1", doc, nil) if err != nil { b.Fatalf("failed to upsert %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := globalCollection.Touch("upsert-touch-1", 10, nil) if err != nil { b.Fatalf("failed to touch %v", err) } } }) } gocb-2.6.3/collection_crud_test.go000066400000000000000000002237711441755043100172120ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "reflect" "strconv" "strings" "testing" "time" "github.com/couchbase/gocbcore/v10/memd" "github.com/stretchr/testify/mock" "github.com/couchbase/gocbcore/v10" ) func (suite *IntegrationTestSuite) TestErrorNonExistant() { suite.skipIfUnsupported(KeyValueFeature) res, err := globalCollection.Get("doesnt-exist", nil) if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Expected error to be non-nil") } suite.Assert().Nil(res) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.AssertKvOpSpan(nilParents[0], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestErrorDoubleInsert() { suite.skipIfUnsupported(KeyValueFeature) docId := generateDocId("doubleInsert") _, err := globalCollection.Insert(docId, "test", nil) if err != nil { suite.T().Fatalf("Expected error to be nil but was %v", err) } _, err = globalCollection.Insert(docId, "test", nil) if err == nil { suite.T().Fatalf("Expected error to be non-nil") } if !errors.Is(err, ErrDocumentExists) { suite.T().Fatalf("Expected error to be DocumentExists but is %s", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "insert", 2, false) } func (suite *IntegrationTestSuite) TestExpiryConversions() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } type tCase struct { name string expiry time.Duration } tCases := []tCase{ { name: "TestExpiryConversionsUnder30Days", expiry: 5 * time.Second, }, { name: "TestExpiryConversions30Days", expiry: 30 * (24 * time.Hour), }, { name: "TestExpiryConversionsOver30Days", expiry: 31 * (24 * time.Hour), }, } for _, tCase := range tCases { suite.T().Run(tCase.name, func(te *testing.T) { res, err := globalCollection.Upsert(tCase.name, doc, &UpsertOptions{ Expiry: tCase.expiry, }) if err != nil { te.Fatalf("Error running Upsert: %v", err) } if res.Cas() == 0 { te.Fatalf("Insert CAS was 0") } start := time.Now() spec := []LookupInSpec{ GetSpec("$document", &GetSpecOptions{IsXattr: true}), } if globalCluster.SupportsFeature(HLCFeature) { spec = append(spec, GetSpec("$vbucket.HLC", &GetSpecOptions{IsXattr: true})) } lookupRes, err := globalCollection.LookupIn( tCase.name, spec, nil, ) if err != nil { te.Fatalf("Error running LookupIn: %v", err) return } exp := struct { Expiration int64 `json:"exptime"` }{} err = lookupRes.ContentAt(0, &exp) if err != nil { te.Fatalf("Error running ContentAt: %v", err) } var actualExpirySecs float64 if globalCluster.SupportsFeature(HLCFeature) { hlcStr := struct { Now string `json:"now"` }{} err = lookupRes.ContentAt(1, &hlcStr) if err != nil { te.Fatalf("Error running ContentAt: %v", err) } hlc, err := strconv.Atoi(hlcStr.Now) if err != nil { te.Fatalf("Error running Atoi: %v", err) } actualExpirySecs = time.Unix(exp.Expiration, 0).Sub(time.Unix(int64(hlc), 0)).Seconds() } else { actualExpirySecs = time.Unix(exp.Expiration, 0).Sub(start).Seconds() } if actualExpirySecs > (tCase.expiry + (1000 * time.Millisecond)).Seconds() { te.Fatalf("Expected expiry to be less than %f but was %f", tCase.expiry.Seconds(), actualExpirySecs) } if actualExpirySecs < (tCase.expiry - (2500 * time.Millisecond)).Seconds() { te.Fatalf("Expected expiry to be greater than %f but was %f", tCase.expiry.Seconds(), actualExpirySecs) } }) } suite.AssertKVMetrics(meterNameCBOperations, "upsert", 3, false) suite.AssertKVMetrics(meterNameCBOperations, "lookup_in", 3, false) } func (suite *IntegrationTestSuite) TestPreserveExpiry() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) suite.skipIfUnsupported(PreserveExpiryFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) suite.Require().Nil(err, err) start := time.Now() mutRes, err := globalCollection.Upsert("preservettl", doc, &UpsertOptions{Expiry: 25 * time.Second}) suite.Require().Nil(err, err) suite.Assert().NotZero(mutRes.Cas()) mutRes, err = globalCollection.Upsert("preservettl", doc, &UpsertOptions{PreserveExpiry: true}) suite.Require().Nil(err, err) suite.Assert().NotZero(mutRes.Cas()) insertedDoc, err := globalCollection.Get("preservettl", &GetOptions{WithExpiry: true}) suite.Require().Nil(err, err) suite.Assert().InDelta(start.Add(25*time.Second).Unix(), insertedDoc.ExpiryTime().Unix(), 5) mutRes, err = globalCollection.Replace("preservettl", doc, &ReplaceOptions{PreserveExpiry: true}) suite.Require().Nil(err, err) if mutRes.Cas() == 0 { suite.T().Fatalf("Replace CAS was 0") } replacedDoc, err := globalCollection.Get("preservettl", &GetOptions{WithExpiry: true}) suite.Require().Nil(err, err) suite.Assert().InDelta(start.Add(25*time.Second).Unix(), replacedDoc.ExpiryTime().Unix(), 5) } func (suite *IntegrationTestSuite) TestInsertGetWithExpiry() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) docId := generateDocId("expiryDoc") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } start := time.Now() mutRes, err := globalCollection.Insert(docId, doc, &InsertOptions{Expiry: 10 * time.Second}) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } insertedDoc, err := globalCollection.Get(docId, &GetOptions{WithExpiry: true}) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } end := time.Now() var insertedDocContent testBeerDocument err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != insertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } if suite.Assert().NotNil(insertedDoc.Expiry()) { suite.Assert().InDelta(end.Sub(start).Seconds(), insertedDoc.Expiry().Seconds(), float64(1*time.Second)) } suite.Assert().InDelta(start.Add(10*time.Second).Second(), insertedDoc.ExpiryTime().Second(), float64(1*time.Second)) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) span := nilParents[1] suite.AssertKvSpan(span, "get", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "lookup_in") lookupSpans := span.Spans["lookup_in"] suite.Require().Equal(len(lookupSpans), 1) suite.AssertKvOpSpan(lookupSpans[0], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "insert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestUpsertGetWithExpiryTranscoder() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) b := []byte("abinarydocument") tcoder := NewRawBinaryTranscoder() start := time.Now() mutRes, err := globalCollection.Upsert("expiryTranscoderDoc", b, &UpsertOptions{ Expiry: 10 * time.Second, Transcoder: tcoder, }) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } insertedDoc, err := globalCollection.Get("expiryTranscoderDoc", &GetOptions{ WithExpiry: true, Transcoder: tcoder, }) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } end := time.Now() var actualB []byte suite.Require().Nil(insertedDoc.Content(&actualB)) suite.Assert().Equal(string(b), string(actualB)) if suite.Assert().NotNil(insertedDoc.Expiry()) { suite.Assert().InDelta(end.Sub(start).Seconds(), insertedDoc.Expiry().Seconds(), float64(1*time.Second)) } suite.Assert().InDelta(start.Add(10*time.Second).Second(), insertedDoc.ExpiryTime().Second(), float64(1*time.Second)) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) span := nilParents[1] suite.AssertKvSpan(span, "get", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "lookup_in") lookupSpans := span.Spans["lookup_in"] suite.Require().Equal(len(lookupSpans), 1) suite.AssertKvOpSpan(lookupSpans[0], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestInsertGetProjection() { suite.skipIfUnsupported(KeyValueFeature) type PersonDimensions struct { Height int `json:"height"` Weight int `json:"weight"` } type Location struct { Lat float32 `json:"lat"` Long float32 `json:"long"` } type HobbyDetails struct { Location Location `json:"location"` } type PersonHobbies struct { Type string `json:"type"` Name string `json:"name"` Details HobbyDetails `json:"details,omitempty"` } type PersonAttributes struct { Hair string `json:"hair"` Dimensions PersonDimensions `json:"dimensions"` Hobbies []PersonHobbies `json:"hobbies"` } type Tracking struct { Locations [][]Location `json:"locations"` Raw [][]float32 `json:"raw"` } type Person struct { Name string `json:"name"` Age int `json:"age"` Animals []string `json:"animals"` Attributes PersonAttributes `json:"attributes"` Tracking Tracking `json:"tracking"` } var person Person err := loadJSONTestDataset("projection_doc", &person) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("projectDoc", person, nil) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } type tCase struct { name string project []string expected Person } testCases := []tCase{ tCase{ name: "string", project: []string{"name"}, expected: Person{Name: person.Name}, }, { name: "int", project: []string{"age"}, expected: Person{Age: person.Age}, }, { name: "array", project: []string{"animals"}, expected: Person{Animals: person.Animals}, }, { name: "array-index1", project: []string{"animals[0]"}, expected: Person{Animals: []string{person.Animals[0]}}, }, { name: "array-index2", project: []string{"animals[1]"}, expected: Person{Animals: []string{person.Animals[1]}}, }, { name: "array-index3", project: []string{"animals[2]"}, expected: Person{Animals: []string{person.Animals[2]}}, }, { name: "full-object-field", project: []string{"attributes"}, expected: Person{Attributes: person.Attributes}, }, { name: "nested-object-field1", project: []string{"attributes.hair"}, expected: Person{ Attributes: PersonAttributes{ Hair: person.Attributes.Hair, }, }, }, { name: "nested-object-field2", project: []string{"attributes.dimensions"}, expected: Person{ Attributes: PersonAttributes{ Dimensions: person.Attributes.Dimensions, }, }, }, { name: "nested-object-field3", project: []string{"attributes.dimensions.height"}, expected: Person{ Attributes: PersonAttributes{ Dimensions: PersonDimensions{ Height: person.Attributes.Dimensions.Height, }, }, }, }, { name: "nested-object-field3", project: []string{"attributes.dimensions.weight"}, expected: Person{ Attributes: PersonAttributes{ Dimensions: PersonDimensions{ Weight: person.Attributes.Dimensions.Weight, }, }, }, }, { name: "nested-object-field4", project: []string{"attributes.hobbies"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: person.Attributes.Hobbies, }, }, }, { name: "nested-array-object-field1", project: []string{"attributes.hobbies[0].type"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Type: person.Attributes.Hobbies[0].Type, }, }, }, }, }, { name: "nested-array-object-field2", project: []string{"attributes.hobbies[1].type"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Type: person.Attributes.Hobbies[1].Type, }, }, }, }, }, { name: "nested-array-object-field3", project: []string{"attributes.hobbies[0].name"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Name: person.Attributes.Hobbies[0].Name, }, }, }, }, }, { name: "nested-array-object-field4", project: []string{"attributes.hobbies[1].name"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Name: person.Attributes.Hobbies[1].Name, }, }, }, }, }, { name: "nested-array-object-field5", project: []string{"attributes.hobbies[1].details"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Details: person.Attributes.Hobbies[1].Details, }, }, }, }, }, { name: "nested-array-object-nested-field1", project: []string{"attributes.hobbies[1].details.location"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Details: HobbyDetails{ Location: person.Attributes.Hobbies[1].Details.Location, }, }, }, }, }, }, { name: "nested-array-object-nested-nested-field1", project: []string{"attributes.hobbies[1].details.location.lat"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Details: HobbyDetails{ Location: Location{ Lat: person.Attributes.Hobbies[1].Details.Location.Lat, }, }, }, }, }, }, }, { name: "nested-array-object-nested-nested-field2", project: []string{"attributes.hobbies[1].details.location.long"}, expected: Person{ Attributes: PersonAttributes{ Hobbies: []PersonHobbies{ { Details: HobbyDetails{ Location: Location{ Long: person.Attributes.Hobbies[1].Details.Location.Long, }, }, }, }, }, }, }, { name: "array-of-arrays-object", project: []string{"tracking.locations[1][1].lat"}, expected: Person{ Tracking: Tracking{ Locations: [][]Location{ { Location{ Lat: person.Tracking.Locations[1][1].Lat, }, }, }, }, }, }, { name: "array-of-arrays-native", project: []string{"tracking.raw[1][1]"}, expected: Person{ Tracking: Tracking{ Raw: [][]float32{ { person.Tracking.Raw[1][1], }, }, }, }, }, } for _, testCase := range testCases { globalTracer.Reset() globalMeter.Reset() suite.T().Run(testCase.name, func(t *testing.T) { doc, err := globalCollection.Get("projectDoc", &GetOptions{ Project: testCase.project, }) if err != nil { t.Fatalf("Get failed, error was %v", err) } var actual Person err = doc.Content(&actual) if err != nil { t.Fatalf("Content failed, error was %v", err) } if !reflect.DeepEqual(actual, testCase.expected) { t.Fatalf("Projection failed, expected %+v but was %+v", testCase.expected, actual) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) span := nilParents[0] suite.AssertKvSpan(span, "get", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "lookup_in") lookupSpans := span.Spans["lookup_in"] suite.Require().Equal(len(lookupSpans), 1) suite.AssertKvOpSpan(lookupSpans[0], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) }) } } func (suite *IntegrationTestSuite) TestInsertGetProjection18Fields() { suite.skipIfUnsupported(KeyValueFeature) docId := generateDocId("insertDoc18Fields") type docType struct { Field1 int `json:"field1"` Field2 int `json:"field2"` Field3 int `json:"field3"` Field4 int `json:"field4"` Field5 int `json:"field5"` Field6 int `json:"field6"` Field7 int `json:"field7"` Field8 int `json:"field8"` Field9 int `json:"field9"` Field10 int `json:"field10"` Field11 int `json:"field11"` Field12 int `json:"field12"` Field13 int `json:"field13"` Field14 int `json:"field14"` Field15 int `json:"field15"` Field16 int `json:"field16"` Field17 int `json:"field17"` Field18 int `json:"field18"` } doc := docType{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, } mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } getDoc, err := globalCollection.Get(docId, &GetOptions{ Project: []string{"field1", "field2", "field3", "field4", "field5", "field6", "field7", "field8", "field9", "field10", "field11", "field12", "field13", "field14", "field15", "field16", "field17"}, }) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var getDocContent map[string]interface{} err = getDoc.Content(&getDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } bytes, err := json.Marshal(doc) if err != nil { suite.T().Fatalf("Marshal failed, error was %v", err) } var originalDocContent map[string]interface{} err = json.Unmarshal(bytes, &originalDocContent) if err != nil { suite.T().Fatalf("Unmarshal failed, error was %v", err) } if len(getDocContent) != 17 { suite.T().Fatalf("Expected doc content to have 17 fields, had %d", len(getDocContent)) } if _, ok := getDocContent["field18"]; ok { suite.T().Fatalf("Expected doc to not contain field18") } for k, v := range originalDocContent { if v != getDocContent[k] && k != "field18" { suite.T().Fatalf("%s not equal, expected %d but was %d", k, v, originalDocContent[k]) } } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get", "", false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "insert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestInsertGetProjection16FieldsExpiry() { suite.skipIfUnsupported(XattrFeature) suite.skipIfUnsupported(KeyValueFeature) type docType struct { Field1 int `json:"field1"` Field2 int `json:"field2"` Field3 int `json:"field3"` Field4 int `json:"field4"` Field5 int `json:"field5"` Field6 int `json:"field6"` Field7 int `json:"field7"` Field8 int `json:"field8"` Field9 int `json:"field9"` Field10 int `json:"field10"` Field11 int `json:"field11"` Field12 int `json:"field12"` Field13 int `json:"field13"` Field14 int `json:"field14"` Field15 int `json:"field15"` Field16 int `json:"field16"` Field17 int `json:"field17"` Field18 int `json:"field18"` } doc := docType{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, } start := time.Now() mutRes, err := globalCollection.Upsert("projectDocTooManyFieldsExpiry", doc, &UpsertOptions{ Expiry: 60 * time.Second, }) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } insertedDoc, err := globalCollection.Get("projectDocTooManyFieldsExpiry", &GetOptions{ Project: []string{"field1", "field2", "field3", "field4", "field5", "field6", "field7", "field8", "field9", "field10", "field11", "field12", "field13", "field14", "field15", "field16"}, WithExpiry: true, }) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } end := time.Now() var insertedDocContent map[string]interface{} err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } bytes, err := json.Marshal(doc) if err != nil { suite.T().Fatalf("Marshal failed, error was %v", err) } var originalDocContent map[string]interface{} err = json.Unmarshal(bytes, &originalDocContent) if err != nil { suite.T().Fatalf("Unmarshal failed, error was %v", err) } if len(insertedDocContent) != 16 { suite.T().Fatalf("Expected doc content to have 16 fields, had %d", len(insertedDocContent)) } for k, v := range originalDocContent { if v != originalDocContent[k] { suite.T().Fatalf("%s not equal, expected %d but was %d", k, v, originalDocContent[k]) } } if suite.Assert().NotNil(insertedDoc.Expiry()) { suite.Assert().InDelta(end.Sub(start).Seconds(), insertedDoc.Expiry().Seconds(), float64(1*time.Second)) } suite.Assert().InDelta(start.Add(10*time.Second).Second(), insertedDoc.ExpiryTime().Second(), float64(1*time.Second)) suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) span := nilParents[1] suite.AssertKvSpan(span, "get", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "lookup_in") lookupSpans := span.Spans["lookup_in"] suite.Require().Equal(len(lookupSpans), 1) suite.AssertKvOpSpan(lookupSpans[0], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestInsertGetProjectionPathMissing() { suite.skipIfUnsupported(KeyValueFeature) docId := generateDocId("projectMissingDoc") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } _, err = globalCollection.Get(docId, &GetOptions{ Project: []string{"name", "thisfielddoesntexist"}, }) if err == nil { suite.T().Fatalf("Get should have failed") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get", "", false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "insert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestInsertGetProjectionTranscoders() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } _, err = globalCollection.Upsert("projectTranscoders", doc, &UpsertOptions{ Expiry: 60 * time.Second, }) type ABVdoc struct { ABV float32 `json:"abv"` } type tCase struct { name string transcoder Transcoder expectErr bool expected interface{} } expectedBytes, err := json.Marshal(ABVdoc{ABV: 7.6}) suite.Require().Nil(err) testCases := []tCase{ { name: "Binary", transcoder: NewRawBinaryTranscoder(), expectErr: true, }, { name: "String", transcoder: NewRawStringTranscoder(), expectErr: true, }, { name: "JSON", transcoder: NewJSONTranscoder(), expected: ABVdoc{ ABV: 7.6, }, }, { name: "Legacy", transcoder: NewLegacyTranscoder(), expected: ABVdoc{ ABV: 7.6, }, }, { name: "RawJSON", transcoder: NewRawJSONTranscoder(), expected: expectedBytes, }, } for _, testCase := range testCases { suite.T().Run(testCase.name, func(t *testing.T) { res, err := globalCollection.Get("projectTranscoders", &GetOptions{ Project: []string{"abv"}, Transcoder: testCase.transcoder, }) if suite.Assert().Nil(err, err) { if reflect.TypeOf(testCase.transcoder) == reflect.TypeOf(NewRawJSONTranscoder()) { var actual []byte err = res.Content(&actual) if suite.Assert().Nil(err, err) { suite.Assert().Equal(testCase.expected, actual) } return } var actual ABVdoc err = res.Content(&actual) if testCase.expectErr { suite.Assert().NotNil(err, err) } else { if suite.Assert().Nil(err, err) { suite.Assert().Equal(testCase.expected, actual) } } } }) } } func (suite *IntegrationTestSuite) TestInsertGet() { suite.skipIfUnsupported(KeyValueFeature) docId := generateDocId("insertDoc") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } insertedDoc, err := globalCollection.Get(docId, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var insertedDocContent testBeerDocument err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != insertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "insert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestExists() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(GetMetaFeature) _, err := globalCollection.Insert("exists", "test", nil) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } existsRes, err := globalCollection.Exists("exists", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if !existsRes.Exists() { suite.T().Fatalf("Expected exists to return true") } _, err = globalCollection.Remove("exists", nil) if err != nil { suite.T().Fatalf("Remove failed, error was %v", err) } existsRes, err = globalCollection.Exists("exists", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if existsRes.Exists() { suite.T().Fatalf("Expected exists to return false") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 4) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "remove", memd.CmdDelete.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "insert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "remove", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "exists", 2, false) } // Following test tests that if a collection is deleted and recreated midway through a set of operations // then the operations will still succeed due to the cid being refreshed under the hood. func (suite *IntegrationTestSuite) TestCollectionRetry() { suite.skipIfUnsupported(CollectionsFeature) suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } collectionName := generateDocId("insertRetry") // cli := globalBucket.sb.getCachedClient() mgr := globalBucket.Collections() err = mgr.CreateCollection(CollectionSpec{ScopeName: "_default", Name: collectionName}, nil) if err != nil { suite.T().Fatalf("Could not create collection: %v", err) } col := globalBucket.Collection(collectionName) // Make sure we've connected to the collection ok success := suite.tryUntil(time.Now().Add(10*time.Second), 500*time.Millisecond, func() bool { mutRes, err := col.Upsert("insertRetryDoc", doc, nil) if err != nil { suite.T().Logf("Upsert failed, error was %v", err) return false } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } return true }) suite.Require().True(success) // The following delete and create will recreate a collection with the same name but a different cid. err = mgr.DropCollection(CollectionSpec{ScopeName: "_default", Name: collectionName}, nil) if err != nil { suite.T().Fatalf("Could not drop collection: %v", err) } err = mgr.CreateCollection(CollectionSpec{ScopeName: "_default", Name: collectionName}, nil) if err != nil { suite.T().Fatalf("Could not create collection: %v", err) } // We've wiped the collection so we need to recreate this doc // We know that this operation can take a bit longer than normal as collections take time to come online. _, err = col.Upsert("insertRetryDoc", doc, &UpsertOptions{ Timeout: 10000 * time.Millisecond, }) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } insertedDoc, err := col.Get("insertRetryDoc", nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var insertedDocContent testBeerDocument err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != insertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, meterValueServiceManagement, "manager_collections_create_collection"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, meterValueServiceManagement, "manager_collections_drop_collection"), 1, false) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 2, true) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestUpsertGetRemove() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(GetMetaFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("upsertDoc", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } upsertedDoc, err := globalCollection.Get("upsertDoc", nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var upsertedDocContent testBeerDocument err = upsertedDoc.Content(&upsertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != upsertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, upsertedDocContent) } existsRes, err := globalCollection.Exists("upsertDoc", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if !existsRes.Exists() { suite.T().Fatalf("Expected exists to return true") } _, err = globalCollection.Remove("upsertDoc", nil) if err != nil { suite.T().Fatalf("Remove failed, error was %v", err) } existsRes, err = globalCollection.Exists("upsertDoc", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if existsRes.Exists() { suite.T().Fatalf("Expected exists to return false") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 5) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "remove", memd.CmdDelete.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[4], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "remove", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "exists", 2, false) } type upsertRetriesStrategy struct { retries int } func (rts *upsertRetriesStrategy) RetryAfter(RetryRequest, RetryReason) RetryAction { rts.retries++ return &WithDurationRetryAction{100 * time.Millisecond} } func (suite *IntegrationTestSuite) TestUpsertRetries() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("getRetryDoc", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } _, err = globalCollection.GetAndLock("getRetryDoc", 1*time.Second, nil) if err != nil { suite.T().Fatalf("GetAndLock failed, error was %v", err) } retryStrategy := &upsertRetriesStrategy{} mutRes, err = globalCollection.Upsert("getRetryDoc", doc, &UpsertOptions{ Timeout: 2100 * time.Millisecond, // Timeout has to be long due to how the server handles unlocking. RetryStrategy: retryStrategy, }) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } if retryStrategy.retries <= 1 { suite.T().Fatalf("Expected retries to be > 1") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get_and_lock", memd.CmdGetLocked.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "upsert", memd.CmdSet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 2, true) suite.AssertKVMetrics(meterNameCBOperations, "get_and_lock", 1, false) } func (suite *IntegrationTestSuite) TestRemoveWithCas() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(GetMetaFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("removeWithCas", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } existsRes, err := globalCollection.Exists("removeWithCas", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if !existsRes.Exists() { suite.T().Fatalf("Expected exists to return true") } _, err = globalCollection.Remove("removeWithCas", &RemoveOptions{Cas: mutRes.Cas() + 0xFECA}) if err == nil { suite.T().Fatalf("Expected remove to fail") } if !errors.Is(err, ErrCasMismatch) { suite.T().Fatalf("Expected error to be CasMismatch but is %s", err) } existsRes, err = globalCollection.Exists("removeWithCas", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if !existsRes.Exists() { suite.T().Fatalf("Expected exists to return true") } _, err = globalCollection.Remove("removeWithCas", &RemoveOptions{Cas: mutRes.Cas()}) if err != nil { suite.T().Fatalf("Remove failed, error was %v", err) } existsRes, err = globalCollection.Exists("removeWithCas", nil) if err != nil { suite.T().Fatalf("Exists failed, error was %v", err) } if existsRes.Exists() { suite.T().Fatalf("Expected exists to return false") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 6) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "remove", memd.CmdDelete.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[4], "remove", memd.CmdDelete.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[5], "exists", memd.CmdGetMeta.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "exists", 3, false) suite.AssertKVMetrics(meterNameCBOperations, "remove", 2, false) } func (suite *IntegrationTestSuite) TestUpsertAndReplace() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("upsertAndReplace", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } insertedDoc, err := globalCollection.Get("upsertAndReplace", nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var insertedDocContent testBeerDocument err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != insertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } doc.Name = "replaced" mutRes, err = globalCollection.Replace("upsertAndReplace", doc, &ReplaceOptions{Cas: mutRes.Cas()}) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } replacedDoc, err := globalCollection.Get("upsertAndReplace", nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var replacedDocContent testBeerDocument err = replacedDoc.Content(&replacedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != replacedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 4) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "replace", memd.CmdReplace.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 2, false) suite.AssertKVMetrics(meterNameCBOperations, "replace", 1, false) } func (suite *IntegrationTestSuite) TestGetAndTouch() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("getAndTouch", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } start := time.Now() lockedDoc, err := globalCollection.GetAndTouch("getAndTouch", 10*time.Second, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var lockedDocContent testBeerDocument err = lockedDoc.Content(&lockedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != lockedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } expireDoc, err := globalCollection.Get("getAndTouch", &GetOptions{WithExpiry: true}) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } end := time.Now() if suite.Assert().NotNil(expireDoc.Expiry()) { suite.Assert().InDelta(end.Sub(start).Seconds(), expireDoc.Expiry().Seconds(), float64(1*time.Second)) } suite.Assert().InDelta(start.Add(10*time.Second).Second(), expireDoc.ExpiryTime().Second(), float64(1*time.Second)) var expireDocContent testBeerDocument err = expireDoc.Content(&expireDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != expireDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get_and_touch", memd.CmdGAT.Name(), false, DurabilityLevelNone) span := nilParents[2] suite.AssertKvSpan(span, "get", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "lookup_in") lookupSpans := span.Spans["lookup_in"] suite.Require().Equal(len(lookupSpans), 1) suite.AssertKvOpSpan(lookupSpans[0], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get_and_touch", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestExpires() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("expires", doc, &UpsertOptions{ Expiry: 1 * time.Second, }) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } globalCluster.TimeTravel(3000 * time.Millisecond) _, err = globalCollection.Get("expires", nil) if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Get should have failed with doc not found but was: %v", err) } } func (suite *IntegrationTestSuite) TestGetAndLock() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("getAndLock", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } lockedDoc, err := globalCollection.GetAndLock("getAndLock", 1*time.Second, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var lockedDocContent testBeerDocument err = lockedDoc.Content(&lockedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != lockedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } mutRes, err = globalCollection.Upsert("getAndLock", doc, &UpsertOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if err == nil { suite.T().Fatalf("Expected error but was nil") } if !errors.Is(err, ErrDocumentLocked) { suite.T().Fatalf("Expected error to be DocumentLocked but is %s", err) } globalCluster.TimeTravel(2000 * time.Millisecond) mutRes, err = globalCollection.Upsert("getAndLock", doc, &UpsertOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 4) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get_and_lock", memd.CmdGetLocked.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "upsert", memd.CmdSet.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "upsert", memd.CmdSet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 3, false) suite.AssertKVMetrics(meterNameCBOperations, "get_and_lock", 1, false) } func (suite *IntegrationTestSuite) TestUnlock() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("unlock", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } lockedDoc, err := globalCollection.GetAndLock("unlock", 1, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var lockedDocContent testBeerDocument err = lockedDoc.Content(&lockedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != lockedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } err = globalCollection.Unlock("unlock", lockedDoc.Cas(), nil) if err != nil { suite.T().Fatalf("Unlock failed, error was %v", err) } mutRes, err = globalCollection.Upsert("unlock", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 4) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get_and_lock", memd.CmdGetLocked.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "unlock", memd.CmdUnlockKey.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[3], "upsert", memd.CmdSet.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 2, false) suite.AssertKVMetrics(meterNameCBOperations, "get_and_lock", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "unlock", 1, false) } func (suite *IntegrationTestSuite) TestUnlockInvalidCas() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("unlockInvalidCas", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } lockedDoc, err := globalCollection.GetAndLock("unlockInvalidCas", 2, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var lockedDocContent testBeerDocument err = lockedDoc.Content(&lockedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != lockedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } err = globalCollection.Unlock("unlockInvalidCas", lockedDoc.Cas()+1, &UnlockOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if err == nil { suite.T().Fatalf("Unlock should have failed") } // The server and the mock do not agree on the error for locked documents. if !errors.Is(err, ErrCasMismatch) && !errors.Is(err, ErrTemporaryFailure) { suite.T().Fatalf("Expected error to be DocumentLocked or TemporaryFailure but was %s", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get_and_lock", memd.CmdGetLocked.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "unlock", memd.CmdUnlockKey.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get_and_lock", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "unlock", 1, false) } func (suite *IntegrationTestSuite) TestDoubleLockFail() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("doubleLock", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } lockedDoc, err := globalCollection.GetAndLock("doubleLock", 1, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var lockedDocContent testBeerDocument err = lockedDoc.Content(&lockedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != lockedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } _, err = globalCollection.GetAndLock("doubleLock", 1, &GetAndLockOptions{ RetryStrategy: newFailFastRetryStrategy(), }) if err == nil { suite.T().Fatalf("Expected GetAndLock to fail") } // The server and the mock do not agree on the error for locked documents. if !errors.Is(err, ErrDocumentLocked) && !errors.Is(err, ErrTemporaryFailure) { suite.T().Fatalf("Expected error to be DocumentLocked or TemporaryFailure but was %s", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "get_and_lock", memd.CmdGetLocked.Name(), false, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[2], "get_and_lock", memd.CmdGetLocked.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get_and_lock", 2, false) } func (suite *IntegrationTestSuite) TestUnlockMissingDocFail() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfServerVersionEquals(srvVer750) err := globalCollection.Unlock("unlockMissing", 123, nil) if err == nil { suite.T().Fatalf("Expected Unlock to fail") } if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Expected error to be DocumentNotFound but was %v", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.AssertKvOpSpan(nilParents[0], "unlock", memd.CmdUnlockKey.Name(), false, DurabilityLevelNone) } func (suite *IntegrationTestSuite) TestTouch() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("touch", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } start := time.Now() touchOut, err := globalCollection.Touch("touch", 3*time.Second, nil) if err != nil { suite.T().Fatalf("Touch failed, error was %v", err) } if touchOut.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } expireDoc, err := globalCollection.Get("touch", &GetOptions{WithExpiry: true}) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } end := time.Now() if suite.Assert().NotNil(expireDoc.Expiry()) { suite.Assert().InDelta(end.Sub(start).Seconds(), expireDoc.Expiry().Seconds(), float64(1*time.Second)) } suite.Assert().InDelta(start.Add(10*time.Second).Second(), expireDoc.ExpiryTime().Second(), float64(1*time.Second)) var expireDocContent testBeerDocument err = expireDoc.Content(&expireDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != expireDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, expireDocContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "touch", memd.CmdTouch.Name(), false, DurabilityLevelNone) span := nilParents[2] suite.AssertKvSpan(span, "get", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "lookup_in") lookupSpans := span.Spans["lookup_in"] suite.Require().Equal(len(lookupSpans), 1) suite.AssertKvOpSpan(lookupSpans[0], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "touch", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get", 1, false) } func (suite *IntegrationTestSuite) TestTouchMissingDocFail() { suite.skipIfUnsupported(KeyValueFeature) _, err := globalCollection.Touch("touchMissing", 3, nil) if err == nil { suite.T().Fatalf("Touch should have failed") } if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Expected error to be KeyNotFoundError but was %v", err) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.AssertKvOpSpan(nilParents[0], "touch", memd.CmdTouch.Name(), false, DurabilityLevelNone) suite.AssertKVMetrics(meterNameCBOperations, "touch", 1, false) } func (suite *IntegrationTestSuite) TestInsertReplicateStartupWait() { suite.skipIfUnsupported(KeyValueFeature) globalCollection.Get("touchMissing", nil) } func (suite *IntegrationTestSuite) TestInsertReplicateToGetAnyReplica() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(ReplicasFeature) docId := generateDocId("insertReplicaDoc") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Insert(docId, doc, &InsertOptions{ PersistTo: 1, Timeout: 5 * time.Second, }) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } insertedDoc, err := globalCollection.GetAnyReplica(docId, nil) if err != nil { suite.T().Fatalf("GetFromReplica failed, error was %v", err) } var insertedDocContent testBeerDocument err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != insertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) span := nilParents[1] suite.AssertKvSpan(span, "get_any_replica", DurabilityLevelNone) suite.Require().Equal(len(span.Spans), 1) suite.Require().Contains(span.Spans, "get_all_replicas") allReplicasSpans := span.Spans["get_all_replicas"] suite.Require().GreaterOrEqual(len(allReplicasSpans), 1) suite.Require().Contains(allReplicasSpans[0].Spans, "get_replica") getReplicaSpans := allReplicasSpans[0].Spans["get_replica"] suite.Require().GreaterOrEqual(len(getReplicaSpans), 2) // We don't actually know which of these will win. for _, span := range getReplicaSpans { suite.Require().Equal(1, len(span.Spans)) suite.AssertKvSpan(span, "get_replica", DurabilityLevelNone) // We don't know which span was actually cancelled so we don't check the CMD spans. } suite.AssertKVMetrics(meterNameCBOperations, "insert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get_any_replica", 1, false) // We can't reliably check the metrics for the get cmd spans, as we don't know which one will have won. } func (suite *IntegrationTestSuite) TestInsertReplicateToGetAllReplicas() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(ReplicasFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } agent, err := globalCollection.getKvProvider() if err != nil { suite.T().Fatalf("Failed to get kv provider, was %v", err) } snapshot, err := globalCollection.waitForConfigSnapshot(context.Background(), time.Now().Add(5*time.Second), agent) if err != nil { suite.T().Fatalf("Failed to get config snapshot, was %v", err) } numReplicas, err := snapshot.NumReplicas() if err != nil { suite.T().Fatalf("Failed to get numReplicas, was %v", err) } expectedReplicas := numReplicas + 1 mutRes, err := globalCollection.Upsert("insertAllReplicaDoc", doc, &UpsertOptions{ PersistTo: uint(expectedReplicas), }) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } stream, err := globalCollection.GetAllReplicas("insertAllReplicaDoc", &GetAllReplicaOptions{ Timeout: 25 * time.Second, }) if err != nil { suite.T().Fatalf("GetAllReplicas failed, error was %v", err) } actualReplicas := 0 numMasters := 0 for { insertedDoc := stream.Next() if insertedDoc == nil { break } actualReplicas++ if !insertedDoc.IsReplica() { numMasters++ } var insertedDocContent testBeerDocument err = insertedDoc.Content(&insertedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != insertedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, insertedDocContent) } } err = stream.Close() if err != nil { suite.T().Fatalf("Expected stream close to not error, was %v", err) } if expectedReplicas != actualReplicas { suite.T().Fatalf("Expected replicas to be %d but was %d", expectedReplicas, actualReplicas) } if numMasters != 1 { suite.T().Fatalf("Expected number of masters to be 1 but was %d", numMasters) } suite.AssertKVMetrics(meterNameCBOperations, "upsert", 1, false) suite.AssertKVMetrics(meterNameCBOperations, "get_all_replicas", 1, false) } func (suite *IntegrationTestSuite) TestDurabilityGetFromAnyReplica() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(DurabilityFeature) suite.skipIfUnsupported(ReplicasFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } type CasResult interface { Cas() Cas } type tCase struct { name string method string args []interface{} expectCas bool expectedError error expectKeyNotFound bool expectedDurability DurabilityLevel } testCases := []tCase{ { name: "upsertDurabilityMajorityDoc", method: "Upsert", args: []interface{}{generateDocId("upsertDurabilityMajorityDoc"), doc, &UpsertOptions{ DurabilityLevel: DurabilityLevelMajority, // MB-41616: For some reason the first durable request after a lot of collections activity takes longer. Timeout: 10 * time.Second, }}, expectCas: true, expectedError: nil, expectKeyNotFound: false, expectedDurability: DurabilityLevelMajority, }, { name: "insertDurabilityLevelPersistToMajority", method: "Insert", args: []interface{}{generateDocId("insertDurabilityLevelPersistToMajority"), doc, &InsertOptions{ DurabilityLevel: DurabilityLevelPersistToMajority, }}, expectCas: true, expectedError: nil, expectKeyNotFound: false, expectedDurability: DurabilityLevelPersistToMajority, }, { name: "insertDurabilityMajorityDoc", method: "Insert", args: []interface{}{generateDocId("insertDurabilityMajorityDoc"), doc, &InsertOptions{ DurabilityLevel: DurabilityLevelMajority, }}, expectCas: true, expectedError: nil, expectKeyNotFound: false, expectedDurability: DurabilityLevelMajority, }, { name: "insertDurabilityMajorityAndPersistOnMasterDoc", method: "Insert", args: []interface{}{generateDocId("insertDurabilityMajorityAndPersistOnMasterDoc"), doc, &InsertOptions{ DurabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, }}, expectCas: true, expectedError: nil, expectKeyNotFound: false, expectedDurability: DurabilityLevelMajorityAndPersistOnMaster, }, { name: "upsertDurabilityLevelPersistToMajority", method: "Upsert", args: []interface{}{generateDocId("upsertDurabilityLevelPersistToMajority"), doc, &UpsertOptions{ DurabilityLevel: DurabilityLevelPersistToMajority, }}, expectCas: true, expectedError: nil, expectKeyNotFound: false, expectedDurability: DurabilityLevelPersistToMajority, }, { name: "upsertDurabilityMajorityAndPersistOnMasterDoc", method: "Upsert", args: []interface{}{generateDocId("upsertDurabilityMajorityAndPersistOnMasterDoc"), doc, &UpsertOptions{ DurabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, }}, expectCas: true, expectedError: nil, expectKeyNotFound: false, expectedDurability: DurabilityLevelMajorityAndPersistOnMaster, }, } for _, tCase := range testCases { suite.T().Run(tCase.name, func(te *testing.T) { globalTracer.Reset() args := make([]reflect.Value, len(tCase.args)) for i := range tCase.args { args[i] = reflect.ValueOf(tCase.args[i]) } retVals := reflect.ValueOf(globalCollection).MethodByName(tCase.method).Call(args) if len(retVals) != 2 { te.Fatalf("Method call should have returned 2 values but returned %d", len(retVals)) } var retErr error if retVals[1].Interface() != nil { var ok bool retErr, ok = retVals[1].Interface().(error) if ok { if err != nil { te.Fatalf("Method call returned error: %v", err) } } else { te.Fatalf("Could not type assert second returned value to error") } } if retErr != tCase.expectedError { te.Fatalf("Expected error to be %v but was %v", tCase.expectedError, retErr) } if tCase.expectCas { if val, ok := retVals[0].Interface().(CasResult); ok { if val.Cas() == 0 { te.Fatalf("CAS value was 0") } } else { te.Fatalf("Could not assert result to CasResult type") } } _, err := globalCollection.GetAnyReplica(tCase.args[0].(string), nil) if tCase.expectKeyNotFound { if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Expected GetFromReplica to not find a key but got error %v", err) } } else { if err != nil { te.Fatalf("GetFromReplica failed, error was %v", err) } } var cmdName string if tCase.method == "Upsert" { cmdName = memd.CmdSet.Name() } else { cmdName = memd.CmdAdd.Name() } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], strings.ToLower(tCase.method), cmdName, true, tCase.expectedDurability) // GetAnyReplica tracing is a pain to test and we do it elsewhere so we don't bother here. }) } } func (suite *UnitTestSuite) TestGetErrorCollectionUnknown() { var doc testBreweryDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not load dataset: %v", err) } pendingOp := new(mockPendingOp) pendingOp.AssertNotCalled(suite.T(), "Cancel", mock.AnythingOfType("error")) provider := new(mockKvProvider) provider. On("Get", mock.AnythingOfType("gocbcore.GetOptions"), mock.AnythingOfType("gocbcore.GetCallback")). Run(func(args mock.Arguments) { cb := args.Get(1).(gocbcore.GetCallback) cb(nil, gocbcore.ErrCollectionNotFound) }). Return(pendingOp, nil) col := suite.collection("mock", "", "", provider) res, err := col.Get("getDocErrCollectionUnknown", nil) if err == nil { suite.T().Fatalf("Get didn't error") } if res != nil { suite.T().Fatalf("Result should have been nil") } if !errors.Is(err, ErrCollectionNotFound) { suite.T().Fatalf("Error should have been collection missing but was %v", err) } } func (suite *IntegrationTestSuite) TestBasicCrudContext() { suite.skipIfUnsupported(KeyValueFeature) ctx, cancel := context.WithCancel(context.Background()) cancel() res, err := globalCollection.Upsert("context", "test", &UpsertOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) defer cancel() // This is ugly but caves on windows seems to be able to run ops instantaneously (time accuracy). time.Sleep(10 * time.Millisecond) res, err = globalCollection.Upsert("context", "test", &UpsertOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(res) ctx, cancel = context.WithCancel(context.Background()) cancel() getRes, err := globalCollection.Get("context", &GetOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(getRes) ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(1*time.Nanosecond)) defer cancel() // This is ugly but caves on windows seems to be able to run ops instantaneously (time accuracy). time.Sleep(10 * time.Millisecond) getRes, err = globalCollection.Get("context", &GetOptions{ Context: ctx, }) if !errors.Is(err, ErrRequestCanceled) { suite.T().Fatalf("Expected error to be canceled but was %v", err) } suite.Require().Nil(getRes) } func (suite *UnitTestSuite) TestGetErrorProperties() { pendingOp := new(mockPendingOp) pendingOp.AssertNotCalled(suite.T(), "Cancel", mock.AnythingOfType("error")) expectedErr := &gocbcore.KeyValueError{ InnerError: gocbcore.ErrDocumentNotFound, // Doesn't map perfectly but it's good enough StatusCode: 0x01, DocumentKey: "someid", BucketName: "default", ScopeName: "_default", CollectionName: "_default", CollectionID: 0, ErrorName: "KEY_ENOENT ", ErrorDescription: "Not Found", Opaque: 0x25, Context: "", Ref: "", RetryReasons: []gocbcore.RetryReason{gocbcore.KVTemporaryFailureRetryReason}, RetryAttempts: 1, LastDispatchedTo: "10.112.194.102:11210", LastDispatchedFrom: "10.112.194.1:11210", LastConnectionID: "d80aa17aa2577d3d/87aa643382554811", } provider := new(mockKvProvider) provider. On("Get", mock.AnythingOfType("gocbcore.GetOptions"), mock.AnythingOfType("gocbcore.GetCallback")). Run(func(args mock.Arguments) { cb := args.Get(1).(gocbcore.GetCallback) cb(nil, expectedErr) }). Return(pendingOp, nil) col := suite.collection("mock", "", "", provider) res, err := col.Get("someid", nil) if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Error should have been document not found but was %s", err) } var kvErr *KeyValueError if !errors.As(err, &kvErr) { suite.T().Fatalf("Error should have been KeyValueError but was %s", err) } suite.Assert().Equal(expectedErr.StatusCode, kvErr.StatusCode) suite.Assert().Equal(expectedErr.DocumentKey, kvErr.DocumentID) suite.Assert().Equal(expectedErr.BucketName, kvErr.BucketName) suite.Assert().Equal(expectedErr.ScopeName, kvErr.ScopeName) suite.Assert().Equal(expectedErr.CollectionName, kvErr.CollectionName) suite.Assert().Equal(expectedErr.CollectionID, kvErr.CollectionID) suite.Assert().Equal(expectedErr.ErrorName, kvErr.ErrorName) suite.Assert().Equal(expectedErr.ErrorDescription, kvErr.ErrorDescription) suite.Assert().Equal(expectedErr.Opaque, kvErr.Opaque) suite.Assert().Equal(expectedErr.Context, kvErr.Context) suite.Assert().Equal(expectedErr.Ref, kvErr.Ref) suite.Assert().Equal([]RetryReason{KVTemporaryFailureRetryReason}, kvErr.RetryReasons) suite.Assert().Equal(expectedErr.RetryAttempts, kvErr.RetryAttempts) suite.Assert().Equal(expectedErr.LastDispatchedTo, kvErr.LastDispatchedTo) suite.Assert().Equal(expectedErr.LastDispatchedFrom, kvErr.LastDispatchedFrom) suite.Assert().Equal(expectedErr.LastConnectionID, kvErr.LastConnectionID) suite.Assert().Nil(res) } func (suite *UnitTestSuite) TestExpiryConversion5Seconds() { pendingOp := new(mockPendingOp) pendingOp.AssertNotCalled(suite.T(), "Cancel", mock.AnythingOfType("error")) provider := new(mockKvProvider) provider. On("Set", mock.AnythingOfType("gocbcore.SetOptions"), mock.AnythingOfType("gocbcore.StoreCallback")). Run(func(args mock.Arguments) { opts := args.Get(0).(gocbcore.SetOptions) cb := args.Get(1).(gocbcore.StoreCallback) suite.Assert().Equal(uint32(5), opts.Expiry) cb(&gocbcore.StoreResult{ Cas: gocbcore.Cas(123), }, nil) }). Return(pendingOp, nil) col := suite.collection("mock", "", "", provider) res, err := col.Upsert("someid", "someval", &UpsertOptions{ Expiry: 5 * time.Second, }) suite.Require().Nil(err, err) suite.Assert().Equal(Cas(123), res.Cas()) } func (suite *UnitTestSuite) TestExpiryConversion500Milliseconds() { pendingOp := new(mockPendingOp) pendingOp.AssertNotCalled(suite.T(), "Cancel", mock.AnythingOfType("error")) provider := new(mockKvProvider) provider. On("Set", mock.AnythingOfType("gocbcore.SetOptions"), mock.AnythingOfType("gocbcore.StoreCallback")). Run(func(args mock.Arguments) { opts := args.Get(0).(gocbcore.SetOptions) cb := args.Get(1).(gocbcore.StoreCallback) suite.Assert().Equal(uint32(1), opts.Expiry) cb(&gocbcore.StoreResult{ Cas: gocbcore.Cas(123), }, nil) }). Return(pendingOp, nil) col := suite.collection("mock", "", "", provider) res, err := col.Upsert("someid", "someval", &UpsertOptions{ Expiry: 500 * time.Millisecond, }) suite.Require().Nil(err, err) suite.Assert().Equal(Cas(123), res.Cas()) } func (suite *UnitTestSuite) TestExpiryConversion30Days() { pendingOp := new(mockPendingOp) pendingOp.AssertNotCalled(suite.T(), "Cancel", mock.AnythingOfType("error")) expectedTime := time.Now().Add(30 * 24 * time.Hour) provider := new(mockKvProvider) provider. On("Set", mock.AnythingOfType("gocbcore.SetOptions"), mock.AnythingOfType("gocbcore.StoreCallback")). Run(func(args mock.Arguments) { opts := args.Get(0).(gocbcore.SetOptions) cb := args.Get(1).(gocbcore.StoreCallback) if opts.Expiry < uint32(expectedTime.Add(-1*time.Second).Unix()) { suite.T().Fatalf("Expected expiry to be %d but was %d", expectedTime.Second(), opts.Expiry) } if opts.Expiry > uint32(expectedTime.Unix()) { suite.T().Fatalf("Expected expiry to be %d but was %d", expectedTime.Second(), opts.Expiry) } cb(&gocbcore.StoreResult{ Cas: gocbcore.Cas(123), }, nil) }). Return(pendingOp, nil) col := suite.collection("mock", "", "", provider) res, err := col.Upsert("someid", "someval", &UpsertOptions{ Expiry: 30 * 24 * time.Hour, }) suite.Require().Nil(err, err) suite.Assert().Equal(Cas(123), res.Cas()) } func (suite *UnitTestSuite) TestExpiryConversion31Days() { pendingOp := new(mockPendingOp) pendingOp.AssertNotCalled(suite.T(), "Cancel", mock.AnythingOfType("error")) expectedTime := time.Now().Add(31 * 24 * time.Hour) provider := new(mockKvProvider) provider. On("Set", mock.AnythingOfType("gocbcore.SetOptions"), mock.AnythingOfType("gocbcore.StoreCallback")). Run(func(args mock.Arguments) { opts := args.Get(0).(gocbcore.SetOptions) cb := args.Get(1).(gocbcore.StoreCallback) if opts.Expiry < uint32(expectedTime.Add(-1*time.Second).Unix()) { suite.T().Fatalf("Expected expiry to be %d but was %d", expectedTime.Second(), opts.Expiry) } if opts.Expiry > uint32(expectedTime.Unix()) { suite.T().Fatalf("Expected expiry to be %d but was %d", expectedTime.Second(), opts.Expiry) } cb(&gocbcore.StoreResult{ Cas: gocbcore.Cas(123), }, nil) }). Return(pendingOp, nil) col := suite.collection("mock", "", "", provider) res, err := col.Upsert("someid", "someval", &UpsertOptions{ Expiry: 31 * 24 * time.Hour, }) suite.Require().Nil(err, err) suite.Assert().Equal(Cas(123), res.Cas()) } gocb-2.6.3/collection_ds.go000066400000000000000000000333471441755043100156220ustar00rootroot00000000000000package gocb import ( "errors" "fmt" ) // CouchbaseList represents a list document. type CouchbaseList struct { collection *Collection id string } // List returns a new CouchbaseList for the document specified by id. func (c *Collection) List(id string) *CouchbaseList { return &CouchbaseList{ collection: c, id: id, } } // Iterator returns an iterable for all items in the list. func (cl *CouchbaseList) Iterator() ([]interface{}, error) { span := cl.collection.startKvOpTrace("list_iterator", nil, false) defer span.End() return dsListIterator(span, cl.collection, cl.id) } func dsListIterator(span RequestSpan, collection *Collection, id string) ([]interface{}, error) { content, err := collection.Get(id, &GetOptions{ ParentSpan: span, }) if err != nil { return nil, err } var listContents []interface{} err = content.Content(&listContents) if err != nil { return nil, err } return listContents, nil } // At retrieves the value specified at the given index from the list. func (cl *CouchbaseList) At(index int, valuePtr interface{}) error { span := cl.collection.startKvOpTrace("list_at", nil, false) defer span.End() ops := make([]LookupInSpec, 1) ops[0] = GetSpec(fmt.Sprintf("[%d]", index), nil) result, err := cl.collection.LookupIn(cl.id, ops, &LookupInOptions{ ParentSpan: span, }) if err != nil { return err } return result.ContentAt(0, valuePtr) } // RemoveAt removes the value specified at the given index from the list. func (cl *CouchbaseList) RemoveAt(index int) error { span := cl.collection.startKvOpTrace("list_remove_at", nil, false) defer span.End() ops := make([]MutateInSpec, 1) ops[0] = RemoveSpec(fmt.Sprintf("[%d]", index), nil) _, err := cl.collection.MutateIn(cl.id, ops, &MutateInOptions{ ParentSpan: span, }) if err != nil { return err } return nil } // Append appends an item to the list. func (cl *CouchbaseList) Append(val interface{}) error { span := cl.collection.startKvOpTrace("list_append", nil, false) defer span.End() ops := make([]MutateInSpec, 1) ops[0] = ArrayAppendSpec("", val, nil) _, err := cl.collection.MutateIn(cl.id, ops, &MutateInOptions{ StoreSemantic: StoreSemanticsUpsert, ParentSpan: span, }) if err != nil { return err } return nil } // Prepend prepends an item to the list. func (cl *CouchbaseList) Prepend(val interface{}) error { span := cl.collection.startKvOpTrace("list_prepend", nil, false) defer span.End() return dsListPrepend(span, cl.collection, cl.id, val) } func dsListPrepend(span RequestSpan, collection *Collection, id string, val interface{}) error { ops := make([]MutateInSpec, 1) ops[0] = ArrayPrependSpec("", val, nil) _, err := collection.MutateIn(id, ops, &MutateInOptions{ StoreSemantic: StoreSemanticsUpsert, ParentSpan: span, }) if err != nil { return err } return nil } // IndexOf gets the index of the item in the list. func (cl *CouchbaseList) IndexOf(val interface{}) (int, error) { span := cl.collection.startKvOpTrace("list_index_of", nil, false) defer span.End() content, err := cl.collection.Get(cl.id, &GetOptions{ ParentSpan: span, }) if err != nil { return 0, err } var listContents []interface{} err = content.Content(&listContents) if err != nil { return 0, err } for i, item := range listContents { if item == val { return i, nil } } return -1, nil } // Size returns the size of the list. func (cl *CouchbaseList) Size() (int, error) { span := cl.collection.startKvOpTrace("list_size", nil, false) defer span.End() return dsListSize(span, cl.collection, cl.id) } func dsListSize(span RequestSpan, collection *Collection, id string) (int, error) { ops := make([]LookupInSpec, 1) ops[0] = CountSpec("", nil) result, err := collection.LookupIn(id, ops, &LookupInOptions{ ParentSpan: span, }) if err != nil { return 0, err } var count int err = result.ContentAt(0, &count) if err != nil { return 0, err } return count, nil } // Clear clears a list, also removing it. func (cl *CouchbaseList) Clear() error { span := cl.collection.startKvOpTrace("list_clear", nil, false) defer span.End() return dsListClear(span, cl.collection, cl.id) } func dsListClear(span RequestSpan, collection *Collection, id string) error { _, err := collection.Remove(id, &RemoveOptions{ ParentSpan: span, }) if err != nil { return err } return nil } // CouchbaseMap represents a map document. type CouchbaseMap struct { collection *Collection id string } // Map returns a new CouchbaseMap. func (c *Collection) Map(id string) *CouchbaseMap { return &CouchbaseMap{ collection: c, id: id, } } // Iterator returns an iterable for all items in the map. func (cl *CouchbaseMap) Iterator() (map[string]interface{}, error) { span := cl.collection.startKvOpTrace("map_iterator", nil, false) defer span.End() content, err := cl.collection.Get(cl.id, &GetOptions{ ParentSpan: span, }) if err != nil { return nil, err } var mapContents map[string]interface{} err = content.Content(&mapContents) if err != nil { return nil, err } return mapContents, nil } // At retrieves the item for the given id from the map. func (cl *CouchbaseMap) At(id string, valuePtr interface{}) error { span := cl.collection.startKvOpTrace("map_at", nil, false) defer span.End() ops := make([]LookupInSpec, 1) ops[0] = GetSpec(id, nil) result, err := cl.collection.LookupIn(cl.id, ops, &LookupInOptions{ ParentSpan: span, }) if err != nil { return err } return result.ContentAt(0, valuePtr) } // Add adds an item to the map. func (cl *CouchbaseMap) Add(id string, val interface{}) error { span := cl.collection.startKvOpTrace("map_add", nil, false) defer span.End() ops := make([]MutateInSpec, 1) ops[0] = UpsertSpec(id, val, nil) _, err := cl.collection.MutateIn(cl.id, ops, &MutateInOptions{ StoreSemantic: StoreSemanticsUpsert, ParentSpan: span, }) if err != nil { return err } return nil } // Remove removes an item from the map. func (cl *CouchbaseMap) Remove(id string) error { span := cl.collection.startKvOpTrace("map_remove", nil, false) defer span.End() ops := make([]MutateInSpec, 1) ops[0] = RemoveSpec(id, nil) _, err := cl.collection.MutateIn(cl.id, ops, &MutateInOptions{ ParentSpan: span, }) if err != nil { return err } return nil } // Exists verifies whether or a id exists in the map. func (cl *CouchbaseMap) Exists(id string) (bool, error) { span := cl.collection.startKvOpTrace("map_exists", nil, false) defer span.End() ops := make([]LookupInSpec, 1) ops[0] = ExistsSpec(id, nil) result, err := cl.collection.LookupIn(cl.id, ops, nil) if err != nil { return false, err } return result.Exists(0), nil } // Size returns the size of the map. func (cl *CouchbaseMap) Size() (int, error) { span := cl.collection.startKvOpTrace("map_size", nil, false) defer span.End() ops := make([]LookupInSpec, 1) ops[0] = CountSpec("", nil) result, err := cl.collection.LookupIn(cl.id, ops, &LookupInOptions{ ParentSpan: span, }) if err != nil { return 0, err } var count int err = result.ContentAt(0, &count) if err != nil { return 0, err } return count, nil } // Keys returns all of the keys within the map. func (cl *CouchbaseMap) Keys() ([]string, error) { span := cl.collection.startKvOpTrace("map_keys", nil, false) defer span.End() content, err := cl.collection.Get(cl.id, &GetOptions{ ParentSpan: span, }) if err != nil { return nil, err } var mapContents map[string]interface{} err = content.Content(&mapContents) if err != nil { return nil, err } var keys []string for id := range mapContents { keys = append(keys, id) } return keys, nil } // Values returns all of the values within the map. func (cl *CouchbaseMap) Values() ([]interface{}, error) { span := cl.collection.startKvOpTrace("map_values", nil, false) defer span.End() content, err := cl.collection.Get(cl.id, nil) if err != nil { return nil, err } var mapContents map[string]interface{} err = content.Content(&mapContents) if err != nil { return nil, err } var values []interface{} for _, val := range mapContents { values = append(values, val) } return values, nil } // Clear clears a map, also removing it. func (cl *CouchbaseMap) Clear() error { span := cl.collection.startKvOpTrace("map_clear", nil, false) defer span.End() _, err := cl.collection.Remove(cl.id, &RemoveOptions{ ParentSpan: span, }) if err != nil { return err } return nil } // CouchbaseSet represents a set document. type CouchbaseSet struct { id string collection *Collection } // Set returns a new CouchbaseSet. func (c *Collection) Set(id string) *CouchbaseSet { return &CouchbaseSet{ id: id, collection: c, } } // Iterator returns an iterable for all items in the set. func (cs *CouchbaseSet) Iterator() ([]interface{}, error) { span := cs.collection.startKvOpTrace("set_iterator", nil, false) defer span.End() return dsListIterator(span, cs.collection, cs.id) } // Add adds a value to the set. func (cs *CouchbaseSet) Add(val interface{}) error { span := cs.collection.startKvOpTrace("set_add", nil, false) defer span.End() ops := make([]MutateInSpec, 1) ops[0] = ArrayAddUniqueSpec("", val, nil) _, err := cs.collection.MutateIn(cs.id, ops, &MutateInOptions{ StoreSemantic: StoreSemanticsUpsert, ParentSpan: span, }) if err != nil { return err } return nil } // Remove removes an value from the set. func (cs *CouchbaseSet) Remove(val string) error { span := cs.collection.startKvOpTrace("set_remove", nil, false) defer span.End() for i := 0; i < 16; i++ { content, err := cs.collection.Get(cs.id, &GetOptions{ ParentSpan: span, }) if err != nil { return err } cas := content.Cas() var setContents []interface{} err = content.Content(&setContents) if err != nil { return err } indexToRemove := -1 for i, item := range setContents { if item == val { indexToRemove = i } } if indexToRemove > -1 { ops := make([]MutateInSpec, 1) ops[0] = RemoveSpec(fmt.Sprintf("[%d]", indexToRemove), nil) _, err = cs.collection.MutateIn(cs.id, ops, &MutateInOptions{ Cas: cas, ParentSpan: span, }) if errors.Is(err, ErrCasMismatch) || errors.Is(err, ErrDocumentExists) { continue } if err != nil { return err } } return nil } return errors.New("failed to perform operation after 16 retries") } // Values returns all of the values within the set. func (cs *CouchbaseSet) Values() ([]interface{}, error) { span := cs.collection.startKvOpTrace("set_values", nil, false) defer span.End() content, err := cs.collection.Get(cs.id, &GetOptions{ ParentSpan: span, }) if err != nil { return nil, err } var setContents []interface{} err = content.Content(&setContents) if err != nil { return nil, err } return setContents, nil } // Contains verifies whether or not a value exists within the set. func (cs *CouchbaseSet) Contains(val string) (bool, error) { span := cs.collection.startKvOpTrace("set_contains", nil, false) defer span.End() content, err := cs.collection.Get(cs.id, &GetOptions{ ParentSpan: span, }) if err != nil { return false, err } var setContents []interface{} err = content.Content(&setContents) if err != nil { return false, err } for _, item := range setContents { if item == val { return true, nil } } return false, nil } // Size returns the size of the set func (cs *CouchbaseSet) Size() (int, error) { span := cs.collection.startKvOpTrace("set_size", nil, false) defer span.End() return dsListSize(span, cs.collection, cs.id) } // Clear clears a set, also removing it. func (cs *CouchbaseSet) Clear() error { span := cs.collection.startKvOpTrace("set_clear", nil, false) defer span.End() return dsListClear(span, cs.collection, cs.id) } // CouchbaseQueue represents a queue document. type CouchbaseQueue struct { id string collection *Collection } // Queue returns a new CouchbaseQueue. func (c *Collection) Queue(id string) *CouchbaseQueue { return &CouchbaseQueue{ id: id, collection: c, } } // Iterator returns an iterable for all items in the queue. func (cs *CouchbaseQueue) Iterator() ([]interface{}, error) { span := cs.collection.startKvOpTrace("queue_iterator", nil, false) defer span.End() return dsListIterator(span, cs.collection, cs.id) } // Push pushes a value onto the queue. func (cs *CouchbaseQueue) Push(val interface{}) error { span := cs.collection.startKvOpTrace("queue_push", nil, false) defer span.End() return dsListPrepend(span, cs.collection, cs.id, val) } // Pop pops an items off of the queue. func (cs *CouchbaseQueue) Pop(valuePtr interface{}) error { span := cs.collection.startKvOpTrace("queue_pop", nil, false) defer span.End() for i := 0; i < 16; i++ { ops := make([]LookupInSpec, 1) ops[0] = GetSpec("[-1]", nil) content, err := cs.collection.LookupIn(cs.id, ops, &LookupInOptions{ ParentSpan: span, }) if err != nil { return err } cas := content.Cas() err = content.ContentAt(0, valuePtr) if err != nil { return err } mutateOps := make([]MutateInSpec, 1) mutateOps[0] = RemoveSpec("[-1]", nil) _, err = cs.collection.MutateIn(cs.id, mutateOps, &MutateInOptions{ Cas: cas, ParentSpan: span, }) if errors.Is(err, ErrCasMismatch) || errors.Is(err, ErrDocumentExists) { continue } if err != nil { return err } return nil } return errors.New("failed to perform operation after 16 retries") } // Size returns the size of the queue. func (cs *CouchbaseQueue) Size() (int, error) { span := cs.collection.startKvOpTrace("queue_size", nil, false) defer span.End() return dsListSize(span, cs.collection, cs.id) } // Clear clears a queue, also removing it. func (cs *CouchbaseQueue) Clear() error { span := cs.collection.startKvOpTrace("queue_clear", nil, false) defer span.End() return dsListClear(span, cs.collection, cs.id) } gocb-2.6.3/collection_ds_test.go000066400000000000000000000225011441755043100166470ustar00rootroot00000000000000package gocb func (suite *IntegrationTestSuite) TestListCrud() { suite.skipIfUnsupported(KeyValueFeature) list := globalCollection.List("testList") err := list.Append("test1") if err != nil { suite.T().Fatalf("Failed to append to list %v", err) } err = list.Append("test2") if err != nil { suite.T().Fatalf("Failed to append to list %v", err) } err = list.Prepend("test3") if err != nil { suite.T().Fatalf("Failed to append to list %v", err) } err = list.Append("test4") if err != nil { suite.T().Fatalf("Failed to append to list %v", err) } size, err := list.Size() if err != nil { suite.T().Fatalf("Failed to get size of list %v", err) } if size != 4 { suite.T().Fatalf("Expected list size to be 4 but was %d", size) } index, err := list.IndexOf("test2") if err != nil { suite.T().Fatalf("Failed to get index from list %v", err) } if index != 2 { suite.T().Fatalf("Expected list index to be 2 but was %d", size) } var index2 string err = list.At(2, &index2) if err != nil { suite.T().Fatalf("Failed to get content from list %v", err) } if index2 != "test2" { suite.T().Fatalf("Expected list item to be test2 but was %d", size) } iter, err := list.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for list %v", err) } var items []string for _, item := range iter { items = append(items, item.(string)) } expected := []string{"test3", "test1", "test2", "test4"} for i, item := range items { if expected[i] != item { suite.T().Fatalf("Expected item at %d to be %s but was %s", i, expected[i], item) } } err = list.RemoveAt(2) if err != nil { suite.T().Fatalf("Failed to remove from list %v", err) } size, err = list.Size() if err != nil { suite.T().Fatalf("Failed to get size of list %v", err) } if size != 3 { suite.T().Fatalf("Expected size after remove to be 3 but was %d", size) } iter2, err := list.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for list %v", err) } var items2 []string for _, item := range iter2 { items2 = append(items2, item.(string)) } expected2 := []string{"test3", "test1", "test4"} for i, item := range items2 { if expected2[i] != item { suite.T().Fatalf("Expected item at %d to be %s but was %s", i, expected2[i], item) } } err = list.Clear() if err != nil { suite.T().Fatalf("Failed to clear list %v", err) } } func (suite *IntegrationTestSuite) TestSetCrud() { suite.skipIfUnsupported(KeyValueFeature) set := globalCollection.Set("testSet") err := set.Add("test1") if err != nil { suite.T().Fatalf("Failed to add to set %v", err) } err = set.Add("test2") if err != nil { suite.T().Fatalf("Failed to add to set %v", err) } err = set.Add("test3") if err != nil { suite.T().Fatalf("Failed to add to set %v", err) } err = set.Add("test4") if err != nil { suite.T().Fatalf("Failed to add to set %v", err) } err = set.Add("test4") if err == nil { suite.T().Fatalf("Expected adding an item twice to fail") } size, err := set.Size() if err != nil { suite.T().Fatalf("Failed to get size of set %v", err) } if size != 4 { suite.T().Fatalf("Expected set size to be 4 but was %d", size) } contains, err := set.Contains("test2") if err != nil { suite.T().Fatalf("Failed to get contains from set %v", err) } if !contains { suite.T().Fatalf("Expected contains to be true") } contains, err = set.Contains("test5") if err != nil { suite.T().Fatalf("Failed to get contains from set %v", err) } if contains { suite.T().Fatalf("Expected contains to be false") } iter, err := set.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for set %v", err) } var items []string for _, item := range iter { items = append(items, item.(string)) } expected := map[string]string{"test1": "", "test2": "", "test3": "", "test4": ""} for _, item := range items { _, ok := expected[item] if !ok { suite.T().Fatalf("Unexpected iterator entry %s", item) } } values, err := set.Values() if err != nil { suite.T().Fatalf("Failed to get values for set %v", err) } for _, item := range values { _, ok := expected[item.(string)] if !ok { suite.T().Fatalf("Unexpected values entry %s", item) } } err = set.Remove("test2") if err != nil { suite.T().Fatalf("Failed to remove from set %v", err) } size, err = set.Size() if err != nil { suite.T().Fatalf("Failed to get size of set %v", err) } if size != 3 { suite.T().Fatalf("Expected size after remove to be 3 but was %d", size) } iter2, err := set.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for set %v", err) } var items2 []string for _, item := range iter2 { items2 = append(items2, item.(string)) } expected2 := map[string]string{"test1": "", "test3": "", "test4": ""} for _, item := range items2 { _, ok := expected2[item] if !ok { suite.T().Fatalf("Unexpected iterator entry %s", item) } } err = set.Clear() if err != nil { suite.T().Fatalf("Failed to clear set %v", err) } } func (suite *IntegrationTestSuite) TestQueueCrud() { suite.skipIfUnsupported(KeyValueFeature) queue := globalCollection.Queue("testQueue") err := queue.Push("test1") if err != nil { suite.T().Fatalf("Failed to push to queue %v", err) } err = queue.Push("test2") if err != nil { suite.T().Fatalf("Failed to push to queue %v", err) } err = queue.Push("test3") if err != nil { suite.T().Fatalf("Failed to push to queue %v", err) } err = queue.Push("test4") if err != nil { suite.T().Fatalf("Failed to push to queue %v", err) } size, err := queue.Size() if err != nil { suite.T().Fatalf("Failed to get size of queue %v", err) } if size != 4 { suite.T().Fatalf("Expected queue size to be 4 but was %d", size) } iter, err := queue.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for queue %v", err) } var items []string for _, item := range iter { items = append(items, item.(string)) } expected := []string{"test4", "test3", "test2", "test1"} for i, item := range items { if expected[i] != item { suite.T().Fatalf("Expected item at %d to be %s but was %s", i, expected[i], item) } } var removed string err = queue.Pop(&removed) if err != nil { suite.T().Fatalf("Failed to remove from queue %v", err) } if removed != "test1" { suite.T().Fatalf("Expected test1 to be pop'd but was %s", removed) } size, err = queue.Size() if err != nil { suite.T().Fatalf("Failed to get size of queue %v", err) } if size != 3 { suite.T().Fatalf("Expected size after remove to be 3 but was %d", size) } iter2, err := queue.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for queue %v", err) } var items2 []string for _, item := range iter2 { items2 = append(items2, item.(string)) } expected2 := []string{"test4", "test3", "test2"} for i, item := range items2 { if expected2[i] != item { suite.T().Fatalf("Expected item at %d to be %s but was %s", i, expected2[i], item) } } err = queue.Clear() if err != nil { suite.T().Fatalf("Failed to clear queue %v", err) } } func (suite *IntegrationTestSuite) TestMapCrud() { suite.skipIfUnsupported(KeyValueFeature) cMap := globalCollection.Map("testMap") err := cMap.Add("test1", "test1val") if err != nil { suite.T().Fatalf("Failed to Add to cMap %v", err) } err = cMap.Add("test2", "test2val") if err != nil { suite.T().Fatalf("Failed to Add to cMap %v", err) } err = cMap.Add("test3", "test3val") if err != nil { suite.T().Fatalf("Failed to Add to cMap %v", err) } err = cMap.Add("test4", "test4val") if err != nil { suite.T().Fatalf("Failed to Add to cMap %v", err) } size, err := cMap.Size() if err != nil { suite.T().Fatalf("Failed to get size of cMap %v", err) } if size != 4 { suite.T().Fatalf("Expected cMap size to be 4 but was %d", size) } var test3Val string err = cMap.At("test3", &test3Val) suite.Assert().Nil(err, err) suite.Assert().Equal("test3val", test3Val) suite.Assert().True(cMap.Exists("test3")) iter, err := cMap.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for cMap %v", err) } expected := map[string]interface{}{"test1": "test1val", "test2": "test2val", "test3": "test3val", "test4": "test4val"} for key, item := range iter { expectedItem, ok := expected[key] if !ok { suite.T().Fatalf("Expected map to contain %s but didn't", key) } if expectedItem != item { suite.T().Fatalf("Expected item at %s to be %s but was %s", key, expectedItem, item) } } err = cMap.Remove("test1") if err != nil { suite.T().Fatalf("Failed to remove from cMap %v", err) } suite.Assert().False(cMap.Exists("test1")) size, err = cMap.Size() if err != nil { suite.T().Fatalf("Failed to get size of cMap %v", err) } if size != 3 { suite.T().Fatalf("Expected size after remove to be 3 but was %d", size) } iter2, err := cMap.Iterator() if err != nil { suite.T().Fatalf("Failed to get iterator for cMap %v", err) } expected2 := map[string]interface{}{"test2": "test2val", "test3": "test3val", "test4": "test4val"} for key, item := range iter2 { expectedItem, ok := expected2[key] if !ok { suite.T().Fatalf("Expected map to contain %s but didn't", key) } if expectedItem != item { suite.T().Fatalf("Expected item at %s to be %s but was %s", key, expectedItem, item) } } err = cMap.Clear() if err != nil { suite.T().Fatalf("Failed to clear map %v", err) } } gocb-2.6.3/collection_dura.go000066400000000000000000000102601441755043100161340ustar00rootroot00000000000000package gocb import ( "context" "sync" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) func (c *Collection) observeOnceSeqNo( ctx context.Context, trace RequestSpan, docID string, mt gocbcore.MutationToken, replicaIdx int, cancelCh chan struct{}, timeout time.Duration, user string, ) (didReplicate, didPersist bool, errOut error) { opm := c.newKvOpManager("observe_once", trace) defer opm.Finish(true) opm.SetDocumentID(docID) opm.SetCancelCh(cancelCh) opm.SetTimeout(timeout) opm.SetImpersonate(user) opm.SetContext(ctx) agent, err := c.getKvProvider() if err != nil { return false, false, err } err = opm.Wait(agent.ObserveVb(gocbcore.ObserveVbOptions{ VbID: mt.VbID, VbUUID: mt.VbUUID, ReplicaIdx: replicaIdx, TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), }, func(res *gocbcore.ObserveVbResult, err error) { if err != nil || res == nil { errOut = opm.EnhanceErr(err) opm.Reject() return } didReplicate = res.CurrentSeqNo >= mt.SeqNo didPersist = res.PersistSeqNo >= mt.SeqNo opm.Resolve(nil) })) if err == nil { errOut = err } return } func (c *Collection) observeOne( ctx context.Context, trace RequestSpan, docID string, mt gocbcore.MutationToken, replicaIdx int, replicaCh, persistCh, cancelCh chan struct{}, timeout time.Duration, user string, ) { sentReplicated := false sentPersisted := false calc := gocbcore.ExponentialBackoff(10*time.Microsecond, 100*time.Millisecond, 0) retries := uint32(0) ObserveLoop: for { select { case <-cancelCh: break ObserveLoop default: // not cancelled yet } didReplicate, didPersist, err := c.observeOnceSeqNo(ctx, trace, docID, mt, replicaIdx, cancelCh, timeout, user) if err != nil { logDebugf("ObserveOnce failed unexpected: %s", err) return } if didReplicate && !sentReplicated { replicaCh <- struct{}{} sentReplicated = true } if didPersist && !sentPersisted { persistCh <- struct{}{} sentPersisted = true } // If we've got persisted and replicated, we can just stop if sentPersisted && sentReplicated { break ObserveLoop } waitTmr := gocbcore.AcquireTimer(calc(retries)) retries++ select { case <-waitTmr.C: gocbcore.ReleaseTimer(waitTmr, true) case <-cancelCh: gocbcore.ReleaseTimer(waitTmr, false) } } } func (c *Collection) waitForDurability( ctx context.Context, trace RequestSpan, docID string, mt gocbcore.MutationToken, replicateTo uint, persistTo uint, deadline time.Time, cancelCh chan struct{}, user string, ) error { opm := c.newKvOpManager("observe", trace) defer opm.Finish(true) opm.SetDocumentID(docID) agent, err := c.getKvProvider() if err != nil { return err } snapshot, err := c.waitForConfigSnapshot(ctx, deadline, agent) if err != nil { return err } numReplicas, err := snapshot.NumReplicas() if err != nil { return err } numServers := numReplicas + 1 if replicateTo > uint(numServers-1) || persistTo > uint(numServers) { return opm.EnhanceErr(ErrDurabilityImpossible) } subOpCancelCh := make(chan struct{}, 1) replicaCh := make(chan struct{}, numServers) persistCh := make(chan struct{}, numServers) // If we cancel the sub ops then we need to wait for cancellation to complete before we exit, otherwise // we will attempt to close our span before the child spans complete. var wg sync.WaitGroup for replicaIdx := 0; replicaIdx < numServers; replicaIdx++ { wg.Add(1) go func(ridx int) { c.observeOne(ctx, opm.TraceSpan(), docID, mt, ridx, replicaCh, persistCh, subOpCancelCh, time.Until(deadline), user) wg.Done() }(replicaIdx) } numReplicated := uint(0) numPersisted := uint(0) for { select { case <-replicaCh: numReplicated++ case <-persistCh: numPersisted++ case <-time.After(time.Until(deadline)): // deadline exceeded close(subOpCancelCh) wg.Wait() return opm.EnhanceErr(ErrAmbiguousTimeout) case <-cancelCh: // parent asked for cancellation close(subOpCancelCh) wg.Wait() return opm.EnhanceErr(ErrRequestCanceled) } if numReplicated >= replicateTo && numPersisted >= persistTo { close(subOpCancelCh) wg.Wait() return nil } } } gocb-2.6.3/collection_queryindexes.go000066400000000000000000000151411441755043100177310ustar00rootroot00000000000000package gocb import ( "fmt" "time" ) // CollectionQueryIndexManager provides methods for performing Couchbase query index management against collections. // UNCOMMITTED: This API may change in the future. type CollectionQueryIndexManager struct { base *baseQueryIndexManager bucketName string scopeName string collectionName string } func (qm *CollectionQueryIndexManager) makeKeyspace() string { return fmt.Sprintf("`%s`.`%s`.`%s`", qm.bucketName, qm.scopeName, qm.collectionName) } func (qm *CollectionQueryIndexManager) validateScopeCollection(scope, collection string) error { if scope != "" || collection != "" { return makeInvalidArgumentsError("cannot use scope or collection with collection query index manager") } return nil } // CreateIndex creates an index over the specified fields. // The SDK will automatically escape the provided index keys. For more advanced use cases like index keys using keywords // scope.Query should be used with the query directly. func (qm *CollectionQueryIndexManager) CreateIndex(indexName string, keys []string, opts *CreateQueryIndexOptions) error { if opts == nil { opts = &CreateQueryIndexOptions{} } if indexName == "" { return invalidArgumentsError{ message: "an invalid index name was specified", } } if len(keys) <= 0 { return invalidArgumentsError{ message: "you must specify at least one index-key to index", } } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.CreateIndex( opts.Context, opts.ParentSpan, qm.makeKeyspace(), indexName, keys, createQueryIndexOptions{ IgnoreIfExists: opts.IgnoreIfExists, Deferred: opts.Deferred, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, NumReplicas: opts.NumReplicas, }, ) } // CreatePrimaryIndex creates a primary index. An empty customName uses the default naming. func (qm *CollectionQueryIndexManager) CreatePrimaryIndex(opts *CreatePrimaryQueryIndexOptions) error { if opts == nil { opts = &CreatePrimaryQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.CreateIndex( opts.Context, opts.ParentSpan, qm.makeKeyspace(), opts.CustomName, nil, createQueryIndexOptions{ IgnoreIfExists: opts.IgnoreIfExists, Deferred: opts.Deferred, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, NumReplicas: opts.NumReplicas, }) } // DropIndex drops a specific index by name. func (qm *CollectionQueryIndexManager) DropIndex(indexName string, opts *DropQueryIndexOptions) error { if opts == nil { opts = &DropQueryIndexOptions{} } if indexName == "" { return invalidArgumentsError{ message: "an invalid index name was specified", } } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.DropIndex( opts.Context, opts.ParentSpan, qm.makeKeyspace(), indexName, dropQueryIndexOptions{ IgnoreIfNotExists: opts.IgnoreIfNotExists, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, UseCollectionsSyntax: qm.scopeName != "" || qm.collectionName != "", }) } // DropPrimaryIndex drops the primary index. Pass an empty customName for unnamed primary indexes. func (qm *CollectionQueryIndexManager) DropPrimaryIndex(opts *DropPrimaryQueryIndexOptions) error { if opts == nil { opts = &DropPrimaryQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } return qm.base.DropIndex( opts.Context, opts.ParentSpan, qm.makeKeyspace(), opts.CustomName, dropQueryIndexOptions{ IgnoreIfNotExists: opts.IgnoreIfNotExists, Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, UseCollectionsSyntax: qm.scopeName != "" || qm.collectionName != "", }) } func (qm *CollectionQueryIndexManager) buildGetAllIndexesWhereClause() (string, map[string]interface{}) { var where string if qm.collectionName == "_default" && qm.scopeName == "_default" { where = "((bucket_id=$bucketName AND scope_id=$scopeName AND keyspace_id=$collectionName) OR (bucket_id IS MISSING and keyspace_id=$bucketName)) " } else { where = "(bucket_id=$bucketName AND scope_id=$scopeName AND keyspace_id=$collectionName)" } return where, map[string]interface{}{ "bucketName": qm.bucketName, "scopeName": qm.scopeName, "collectionName": qm.collectionName, } } // GetAllIndexes returns a list of all currently registered indexes. func (qm *CollectionQueryIndexManager) GetAllIndexes(opts *GetAllQueryIndexesOptions) ([]QueryIndex, error) { if opts == nil { opts = &GetAllQueryIndexesOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return nil, err } where, params := qm.buildGetAllIndexesWhereClause() return qm.base.GetAllIndexes( opts.Context, opts.ParentSpan, where, params, getAllQueryIndexesOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }) } // BuildDeferredIndexes builds all indexes which are currently in deferred state. // If no collection and scope names are specified in the options then *only* indexes created on the bucket directly // will be built. func (qm *CollectionQueryIndexManager) BuildDeferredIndexes(opts *BuildDeferredQueryIndexOptions) ([]string, error) { if opts == nil { opts = &BuildDeferredQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return nil, err } where, params := qm.buildGetAllIndexesWhereClause() return qm.base.BuildDeferredIndexes( qm.makeKeyspace(), where, params, buildDeferredQueryIndexOptions{ Timeout: opts.Timeout, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }, ) } // WatchIndexes waits for a set of indexes to come online. func (qm *CollectionQueryIndexManager) WatchIndexes(watchList []string, timeout time.Duration, opts *WatchQueryIndexOptions) error { if opts == nil { opts = &WatchQueryIndexOptions{} } if err := qm.validateScopeCollection(opts.ScopeName, opts.CollectionName); err != nil { return err } where, params := qm.buildGetAllIndexesWhereClause() return qm.base.WatchIndexes( where, params, watchList, timeout, watchQueryIndexOptions{ WatchPrimary: opts.WatchPrimary, RetryStrategy: opts.RetryStrategy, ParentSpan: opts.ParentSpan, Context: opts.Context, }, ) } gocb-2.6.3/collection_queryindexes_test.go000066400000000000000000000202111441755043100207620ustar00rootroot00000000000000package gocb import ( "errors" "time" ) func (suite *IntegrationTestSuite) TestCollectionQueryIndexManagerCrud() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(CollectionsFeature) bucketName := globalBucket.Name() scopeName := generateDocId("scope") colName := generateDocId("collection") colmgr := globalBucket.Collections() err := colmgr.CreateScope(scopeName, nil) suite.Require().Nil(err, err) defer colmgr.DropScope(scopeName, nil) err = colmgr.CreateCollection(CollectionSpec{ ScopeName: scopeName, Name: colName, }, nil) suite.Require().Nil(err, err) mgr := globalBucket.Scope(scopeName).Collection(colName).QueryIndexes() deadline := time.Now().Add(60 * time.Second) for { err = mgr.CreatePrimaryIndex(&CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) if err == nil { break } suite.T().Logf("Failed to create primary index: %s", err) sleepDeadline := time.Now().Add(500 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for create index to succeed") return } } err = mgr.CreatePrimaryIndex(&CreatePrimaryQueryIndexOptions{ IgnoreIfExists: false, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } err = mgr.CreateIndex("testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) err = mgr.CreateIndex("testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } // We create this first to give it a chance to be created by the time we need it. err = mgr.CreateIndex("testIndexDeferred", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, Deferred: true, }) suite.Require().Nil(err, err) indexNames, err := mgr.BuildDeferredIndexes(&BuildDeferredQueryIndexOptions{}) suite.Require().Nil(err, err) suite.Assert().Len(indexNames, 1) err = mgr.WatchIndexes([]string{"testIndexDeferred"}, 30*time.Second, &WatchQueryIndexOptions{}) suite.Require().Nil(err, err) indexes, err := mgr.GetAllIndexes(&GetAllQueryIndexesOptions{}) suite.Require().Nil(err, err) suite.Assert().Len(indexes, 3) var index QueryIndex for _, idx := range indexes { if idx.Name == "testIndex" { index = idx break } } suite.Assert().Equal("testIndex", index.Name) suite.Assert().False(index.IsPrimary) suite.Assert().Equal(QueryIndexTypeGsi, index.Type) suite.Assert().Equal("online", index.State) suite.Assert().Equal(colName, index.Keyspace) suite.Assert().Equal("default", index.Namespace) suite.Assert().Equal(scopeName, index.ScopeName) suite.Assert().Equal(colName, index.CollectionName) suite.Assert().Equal(bucketName, index.BucketName) if suite.Assert().Len(index.IndexKey, 1) { suite.Assert().Equal("`field`", index.IndexKey[0]) } suite.Assert().Empty(index.Condition) suite.Assert().Empty(index.Partition) err = mgr.DropIndex("testIndex", &DropQueryIndexOptions{}) suite.Require().Nil(err, err) err = mgr.DropIndex("testIndex", &DropQueryIndexOptions{}) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } err = mgr.DropPrimaryIndex(&DropPrimaryQueryIndexOptions{}) suite.Require().Nil(err, err) err = mgr.DropPrimaryIndex(&DropPrimaryQueryIndexOptions{}) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_create_scope"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_collections_create_collection"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_create_primary_index"), 2, true) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_create_index"), 3, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_build_deferred_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_watch_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_get_all_indexes"), 1, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_drop_primary_index"), 2, false) suite.AssertMetrics(makeMetricsKey(meterNameCBOperations, "management", "manager_query_drop_index"), 2, false) } func (suite *IntegrationTestSuite) TestCollectionQueryIndexManagerCrudDefaultScopeCollection() { suite.skipIfUnsupported(QueryIndexFeature) suite.skipIfUnsupported(CollectionsFeature) suite.dropAllIndexesAtCollectionLevel() mgr := globalBucket.DefaultCollection().QueryIndexes() deadline := time.Now().Add(60 * time.Second) for { err := mgr.CreatePrimaryIndex(&CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) if err == nil { break } suite.T().Logf("Failed to create primary index: %s", err) sleepDeadline := time.Now().Add(500 * time.Millisecond) if sleepDeadline.After(deadline) { sleepDeadline = deadline } time.Sleep(sleepDeadline.Sub(time.Now())) if sleepDeadline == deadline { suite.T().Errorf("timed out waiting for create index to succeed") return } } err := mgr.CreatePrimaryIndex(&CreatePrimaryQueryIndexOptions{ IgnoreIfExists: false, }) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } err = mgr.CreateIndex("testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) err = mgr.CreateIndex("testIndex", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, }) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Expected index exists error but was %s", err) } // We create this first to give it a chance to be created by the time we need it. err = mgr.CreateIndex("testIndexDeferred", []string{"field"}, &CreateQueryIndexOptions{ IgnoreIfExists: false, Deferred: true, }) suite.Require().Nil(err, err) indexNames, err := mgr.BuildDeferredIndexes(&BuildDeferredQueryIndexOptions{}) suite.Require().Nil(err, err) suite.Assert().Len(indexNames, 1) err = mgr.WatchIndexes([]string{"testIndexDeferred"}, 30*time.Second, &WatchQueryIndexOptions{}) suite.Require().Nil(err, err) indexes, err := mgr.GetAllIndexes(&GetAllQueryIndexesOptions{}) suite.Require().Nil(err, err) suite.Assert().Len(indexes, 3) var index QueryIndex for _, idx := range indexes { if idx.Name == "testIndex" { index = idx break } } suite.Assert().Equal("testIndex", index.Name) suite.Assert().False(index.IsPrimary) suite.Assert().Equal(QueryIndexTypeGsi, index.Type) suite.Assert().Equal("online", index.State) suite.Assert().Equal(globalBucket.bucketName, index.Keyspace) suite.Assert().Equal("default", index.Namespace) suite.Assert().Equal("", index.ScopeName) suite.Assert().Equal("", index.CollectionName) suite.Assert().Equal(globalBucket.Name(), index.BucketName) if suite.Assert().Len(index.IndexKey, 1) { suite.Assert().Equal("`field`", index.IndexKey[0]) } suite.Assert().Empty(index.Condition) suite.Assert().Empty(index.Partition) err = mgr.DropIndex("testIndex", &DropQueryIndexOptions{}) suite.Require().Nil(err, err) err = mgr.DropIndex("testIndex", &DropQueryIndexOptions{}) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } err = mgr.DropPrimaryIndex(&DropPrimaryQueryIndexOptions{}) suite.Require().Nil(err, err) err = mgr.DropPrimaryIndex(&DropPrimaryQueryIndexOptions{}) suite.Require().NotNil(err, err) if !errors.Is(err, ErrIndexNotFound) { suite.T().Fatalf("Expected index not found error but was %s", err) } } gocb-2.6.3/collection_rangescan.go000066400000000000000000000121141441755043100171420ustar00rootroot00000000000000package gocb import ( "context" "time" "github.com/couchbase/gocbcore/v10" ) // ScanOptions are the set of options available to the Scan operation. // VOLATILE: This API is subject to change at any time. type ScanOptions struct { Transcoder Transcoder Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context IDsOnly bool ConsistentWith *MutationState Sort ScanSort // BatchByteLimit specifies a limit to how many bytes are sent from server to client on each partition batch. BatchByteLimit uint32 // BatchItemLimit specifies a limit to how many items are sent from server to client on each partition batch. BatchItemLimit uint32 // Internal: This should never be used and is not supported. Internal struct { User string } } // ScanSort represents the sort order of a Scan operation. type ScanSort uint8 const ( // ScanSortNone indicates that no sorting should be applied during a Scan operation. ScanSortNone ScanSort = iota // ScanSortAscending indicates that ascending sort should be applied during a Scan operation. ScanSortAscending ) // ScanTerm represents a term that can be used during a Scan operation. type ScanTerm struct { Term string Exclusive bool } // ScanTermMinimum represents the minimum value that a ScanTerm can represent. func ScanTermMinimum() *ScanTerm { return &ScanTerm{ Term: "\x00", } } // ScanTermMaximum represents the maximum value that a ScanTerm can represent. func ScanTermMaximum() *ScanTerm { return &ScanTerm{ Term: "\xFF", } } // ScanType represents the mode of execution to use for a Scan operation. type ScanType interface { isScanType() } // NewRangeScanForPrefix creates a new range scan for the given prefix, starting at the prefix and ending at the prefix // plus maximum. // VOLATILE: This API is subject to change at any time. func NewRangeScanForPrefix(prefix string) RangeScan { return RangeScan{ From: &ScanTerm{ Term: prefix, }, To: &ScanTerm{ Term: prefix + "\xFF", }, } } // RangeScan indicates that the Scan operation should scan a range of keys. type RangeScan struct { From *ScanTerm To *ScanTerm } func (rs RangeScan) isScanType() {} func (rs RangeScan) toCore() (*gocbcore.RangeScanCreateRangeScanConfig, error) { to := rs.To from := rs.From rangeOptions := &gocbcore.RangeScanCreateRangeScanConfig{} if from.Exclusive { rangeOptions.ExclusiveStart = []byte(from.Term) } else { rangeOptions.Start = []byte(from.Term) } if to.Exclusive { rangeOptions.ExclusiveEnd = []byte(to.Term) } else { rangeOptions.End = []byte(to.Term) } return rangeOptions, nil } // SamplingScan indicates that the Scan operation should perform random sampling. type SamplingScan struct { Limit uint64 Seed uint64 } func (rs SamplingScan) isScanType() {} func (rs SamplingScan) toCore() (*gocbcore.RangeScanCreateRandomSamplingConfig, error) { if rs.Limit == 0 { return nil, makeInvalidArgumentsError("sampling scan limit must be greater than 0") } return &gocbcore.RangeScanCreateRandomSamplingConfig{ Samples: rs.Limit, Seed: rs.Seed, }, nil } // Scan performs a scan across a Collection, returning a stream of documents. // VOLATILE: This API is subject to change at any time. func (c *Collection) Scan(scanType ScanType, opts *ScanOptions) (*ScanResult, error) { if opts == nil { opts = &ScanOptions{} } agent, err := c.getKvProvider() if err != nil { return nil, err } timeout := opts.Timeout if timeout == 0 { timeout = c.timeoutsConfig.KVScanTimeout } config, err := c.waitForConfigSnapshot(opts.Context, time.Now().Add(timeout), agent) if err != nil { return nil, err } numVbuckets, err := config.NumVbuckets() if err != nil { return nil, err } if numVbuckets == 0 { return nil, makeInvalidArgumentsError("can only use RangeScan with couchbase buckets") } opm, err := c.newRangeScanOpManager(scanType, numVbuckets, agent, opts.ParentSpan, opts.ConsistentWith, opts.IDsOnly, opts.Sort) if err != nil { return nil, err } opm.SetTranscoder(opts.Transcoder) opm.SetContext(opts.Context) opm.SetImpersonate(opts.Internal.User) opm.SetTimeout(opts.Timeout) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetItemLimit(opts.BatchItemLimit) opm.SetByteLimit(opts.BatchByteLimit) if err := opm.CheckReadyForOp(); err != nil { return nil, err } return opm.Scan() } func (c *Collection) waitForConfigSnapshot(ctx context.Context, deadline time.Time, agent kvProvider) (snapOut *gocbcore.ConfigSnapshot, errOut error) { if ctx == nil { ctx = context.Background() } opm := newAsyncOpManager(ctx) err := opm.Wait(agent.WaitForConfigSnapshot(deadline, gocbcore.WaitForConfigSnapshotOptions{}, func(result *gocbcore.WaitForConfigSnapshotResult, err error) { if err != nil { errOut = err opm.Reject() return } snapOut = result.Snapshot opm.Resolve() })) if err != nil { errOut = err } return } gocb-2.6.3/collection_rangescan_test.go000066400000000000000000000445661441755043100202210ustar00rootroot00000000000000package gocb import ( "context" "fmt" "sync" "time" ) func makeBinaryValue(numBytes int) []byte { value := make([]byte, numBytes) for i := 0; i < numBytes; i++ { value[i] = byte(i) } return value } func makeDocIDs(numIDs int, prefix string) map[string]struct{} { docIDs := make(map[string]struct{}) for i := 0; i < numIDs; i++ { docIDs[fmt.Sprintf("%s-%d", prefix, i)] = struct{}{} } return docIDs } func (suite *IntegrationTestSuite) upsertAndCreateMutationState(collection *Collection, docIDs map[string]struct{}, value interface{}, opts *UpsertOptions) *MutationState { mutationState := NewMutationState() var wg sync.WaitGroup wg.Add(len(docIDs)) type tokenAndError struct { token *MutationToken err error } ch := make(chan *tokenAndError, len(docIDs)) for id := range docIDs { go func(id string) { res, err := collection.Upsert(id, value, opts) if err != nil { ch <- &tokenAndError{ err: err, } wg.Done() return } ch <- &tokenAndError{ token: res.MutationToken(), } wg.Done() }(id) } wg.Wait() close(ch) for { tok, ok := <-ch if !ok { break } suite.Require().Nil(tok.err, tok.err) mutationState.Add(*tok.token) } globalTracer.Reset() globalMeter.Reset() return mutationState } func (suite *IntegrationTestSuite) numVbuckets() int { a, err := globalBucket.Internal().IORouter() suite.Require().Nil(err, err) snap, err := a.ConfigSnapshot() suite.Require().Nil(err, err) numVbuckets, err := snap.NumVbuckets() suite.Require().Nil(err, err) return numVbuckets } func (suite *IntegrationTestSuite) verifyRangeScanTracing(topSpan *testSpan, numPartitions int, scanType ScanType, scopeName string, colName string, opts *ScanOptions) { suite.Assert().Equal("range_scan", topSpan.Name) suite.Assert().Equal(11, len(topSpan.Tags)) suite.Assert().Equal("couchbase", topSpan.Tags[spanAttribDBSystemKey]) suite.Assert().Equal(globalConfig.Bucket, topSpan.Tags[spanAttribDBNameKey]) suite.Assert().Equal(scopeName, topSpan.Tags[spanAttribDBScopeNameKey]) suite.Assert().Equal(colName, topSpan.Tags[spanAttribDBCollectionNameKey]) suite.Assert().Equal("kv_scan", topSpan.Tags[spanAttribServiceKey]) suite.Assert().Equal("range_scan", topSpan.Tags[spanAttribOperationKey]) suite.Assert().Equal(numPartitions, topSpan.Tags["num_partitions"]) suite.Assert().Equal(opts.IDsOnly, topSpan.Tags["without_content"]) suite.Assert().True(topSpan.Finished) suite.Assert().Nil(topSpan.ParentContext) switch st := scanType.(type) { case RangeScan: suite.Assert().Equal("range", topSpan.Tags["scan_type"]) suite.Assert().Equal(st.From.Term, topSpan.Tags["from_term"]) suite.Assert().Equal(st.To.Term, topSpan.Tags["to_term"]) case SamplingScan: suite.Assert().Equal("sampling", topSpan.Tags["scan_type"]) suite.Assert().Equal(st.Limit, topSpan.Tags["limit"]) suite.Assert().Equal(st.Seed, topSpan.Tags["seed"]) } itemLimit := opts.BatchItemLimit if itemLimit == 0 { itemLimit = rangeScanDefaultItemLimit } byteLimit := opts.BatchByteLimit if byteLimit == 0 { byteLimit = rangeScanDefaultBytesLimit } suite.Assert().Len(topSpan.Spans, 1) if suite.Assert().Contains(topSpan.Spans, "range_scan_partition") { pSpans := topSpan.Spans["range_scan_partition"] suite.Assert().Len(pSpans, numPartitions) for i, partitionSpan := range pSpans { suite.Assert().Equal("range_scan_partition", partitionSpan.Name) suite.Assert().Equal(uint16(i), partitionSpan.Tags["partition_id"]) suite.Assert().True(partitionSpan.Finished) suite.Assert().Equal(topSpan.Context(), partitionSpan.ParentContext) // Partition spans may only contain a create span if there is no data in the partition. if suite.Assert().GreaterOrEqual(len(partitionSpan.Spans), 1) { if suite.Assert().Contains(partitionSpan.Spans, "range_scan_create") { cSpans := partitionSpan.Spans["range_scan_create"] if suite.Assert().Len(cSpans, 1) { s := cSpans[0] suite.Assert().Equal("range_scan_create", s.Name) suite.Assert().True(s.Finished) suite.Assert().Equal(partitionSpan.Context(), s.ParentContext) numTags := 2 suite.Assert().Equal(opts.IDsOnly, s.Tags["without_content"]) switch st := scanType.(type) { case RangeScan: suite.Assert().Equal("range", s.Tags["scan_type"]) suite.Assert().Equal(st.From.Term, s.Tags["from_term"]) suite.Assert().Equal(st.To.Term, s.Tags["to_term"]) suite.Assert().Equal(st.From.Exclusive, s.Tags["from_exclusive"]) suite.Assert().Equal(st.To.Exclusive, s.Tags["to_exclusive"]) numTags += 4 case SamplingScan: suite.Assert().Equal("sampling", s.Tags["scan_type"]) suite.Assert().Equal(st.Limit, s.Tags["limit"]) suite.Assert().Equal(st.Seed, s.Tags["seed"]) numTags += 2 } suite.Assert().Len(s.Tags, numTags) // if suite.Assert().Contains(s, memd.CmdRangeScanCreate.Name()) { // suite.AssertCmdSpan(s, memd.CmdRangeScanCreate.Name(), 0) // } } } if cSpans, ok := partitionSpan.Spans["range_scan_continue"]; ok { if suite.Assert().Len(cSpans, 1) { s := cSpans[0] suite.Assert().Equal("range_scan_continue", s.Name) suite.Assert().True(s.Finished) suite.Assert().Equal(partitionSpan.Context(), s.ParentContext) suite.Assert().Len(s.Tags, 4) suite.Assert().Equal(itemLimit, s.Tags["item_limit"]) suite.Assert().Equal(byteLimit, s.Tags["byte_limit"]) suite.Assert().Zero(s.Tags["time_limit"]) suite.Assert().NotEmpty(s.Tags["range_scan_id"]) } } } } } } func (suite *IntegrationTestSuite) TestRangeScanRangeWithContent() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) docIDs := makeDocIDs(100, "rangescanwithcontent-") value := "test" suite.upsertAndCreateMutationState(globalCollection, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := RangeScan{ From: &ScanTerm{ Term: "rangescanwithcontent", }, To: &ScanTerm{ Term: "rangescanwithcontent\xFF", }, } opts := &ScanOptions{} res, err := globalCollection.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().NotZero(d.Cas()) var v string err := d.Content(&v) if suite.Assert().Nil(err) { suite.Assert().Equal(value, v) } suite.Assert().Greater(time.Until(d.ExpiryTime()), 15*time.Second) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, globalScope.Name(), globalCollection.Name(), opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } func (suite *IntegrationTestSuite) TestRangeScanRangeWithoutContent() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) docIDs := makeDocIDs(100, "rangescanwithoutcontent-") value := makeBinaryValue(1) suite.upsertAndCreateMutationState(globalCollection, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := RangeScan{ From: &ScanTerm{ Term: "rangescanwithoutcontent", }, To: &ScanTerm{ Term: "rangescanwithoutcontent\xFF", }, } opts := &ScanOptions{ IDsOnly: true, } res, err := globalCollection.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().Zero(d.Cas()) var v interface{} err := d.Content(&v) suite.Assert().ErrorIs(err, ErrInvalidArgument) suite.Assert().Zero(d.ExpiryTime()) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, globalScope.Name(), globalCollection.Name(), opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } func (suite *IntegrationTestSuite) TestRangeScanRangeWithContentBinaryTranscoder() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) docIDs := makeDocIDs(100, "rangescanwithbinarycoder-") value := makeBinaryValue(1) tcoder := NewRawBinaryTranscoder() suite.upsertAndCreateMutationState(globalCollection, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, Transcoder: tcoder, }) scan := RangeScan{ From: &ScanTerm{ Term: "rangescanwithbinarycoder", }, To: &ScanTerm{ Term: "rangescanwithbinarycoder\xFF", }, } opts := &ScanOptions{ Transcoder: tcoder, } res, err := globalCollection.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().NotZero(d.Cas()) var v []byte err := d.Content(&v) if suite.Assert().Nil(err) { suite.Assert().Equal(value, v) } suite.Assert().Greater(time.Until(d.ExpiryTime()), 15*time.Second) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, globalScope.Name(), globalCollection.Name(), opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } func (suite *IntegrationTestSuite) TestRangeScanRangeCancellation() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) // These keys all belong to vbid 12, which means that we can set the item limit and block the stream on // that vbucket from continuing. docIDs := map[string]struct{}{"rangekeysonly-1269": {}, "rangekeysonly-2048": {}, "rangekeysonly-4378": {}, "rangekeysonly-7159": {}, "rangekeysonly-8898": {}, "rangekeysonly-8908": {}, "rangekeysonly-19559": {}, "rangekeysonly-20808": {}, "rangekeysonly-20998": {}, "rangekeysonly-25889": {}} value := makeBinaryValue(1) suite.upsertAndCreateMutationState(globalCollection, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := RangeScan{ From: &ScanTerm{ Term: "rangekeysonly", }, To: &ScanTerm{ Term: "rangekeysonly\xFF", }, } opts := &ScanOptions{ IDsOnly: true, BatchItemLimit: 1, } res, err := globalCollection.Scan(scan, opts) suite.Require().Nil(err, err) stopAt := 5 ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } if len(ids) == stopAt { // At the point of close there should be no errors on the stream. err := res.Close() suite.Assert().Nil(err, err) } ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, 6) err = res.Err() suite.Require().ErrorIs(err, ErrRequestCanceled) } func (suite *IntegrationTestSuite) TestRangeScanSampling() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) scopeName := generateDocId("samplingrangescan") colMgr := globalBucket.Collections() err := colMgr.CreateScope(scopeName, nil) suite.Require().Nil(err, err) defer colMgr.DropScope(scopeName, nil) err = colMgr.CreateCollection(CollectionSpec{ Name: scopeName, ScopeName: scopeName, }, nil) suite.Require().Nil(err, err) docIDs := makeDocIDs(10, generateDocId("samplingscan")) value := "test" col := globalBucket.Scope(scopeName).Collection(scopeName) suite.upsertAndCreateMutationState(col, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := SamplingScan{ Limit: 10, Seed: 50, } opts := &ScanOptions{} res, err := col.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().NotZero(d.Cas()) var v string err := d.Content(&v) if suite.Assert().Nil(err) { suite.Assert().Equal(value, v) } suite.Assert().Greater(time.Until(d.ExpiryTime()), 15*time.Second) suite.Assert().NotEmpty(d.ID()) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, scopeName, scopeName, opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } func (suite *IntegrationTestSuite) TestRangeScanSamplingMutationState() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) scopeName := generateDocId("samplingrangescanmutationstate") colMgr := globalBucket.Collections() err := colMgr.CreateScope(scopeName, nil) suite.Require().Nil(err, err) defer colMgr.DropScope(scopeName, nil) err = colMgr.CreateCollection(CollectionSpec{ Name: scopeName, ScopeName: scopeName, }, nil) suite.Require().Nil(err, err) docIDs := makeDocIDs(10, generateDocId("samplingrangescanmutationstate")) value := "test" col := globalBucket.Scope(scopeName).Collection(scopeName) mutationState := suite.upsertAndCreateMutationState(col, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := SamplingScan{ Limit: 10, Seed: 50, } opts := &ScanOptions{ ConsistentWith: mutationState, } res, err := col.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().NotZero(d.Cas()) var v string err := d.Content(&v) if suite.Assert().Nil(err) { suite.Assert().Equal(value, v) } suite.Assert().Greater(time.Until(d.ExpiryTime()), 15*time.Second) suite.Assert().NotEmpty(d.ID()) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, scopeName, scopeName, opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } func (suite *IntegrationTestSuite) TestRangeScanRangeMutationState() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) docIDs := makeDocIDs(100, "rangescanmutationstate-") value := makeBinaryValue(1) mutationState := suite.upsertAndCreateMutationState(globalCollection, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := RangeScan{ From: &ScanTerm{ Term: "rangescanmutationstate", }, To: &ScanTerm{ Term: "rangescanmutationstate\xFF", }, } opts := &ScanOptions{ IDsOnly: true, ConsistentWith: mutationState, } res, err := globalCollection.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().Zero(d.Cas()) var v interface{} err := d.Content(&v) suite.Assert().ErrorIs(err, ErrInvalidArgument) suite.Assert().Zero(d.ExpiryTime()) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, globalScope.Name(), globalCollection.Name(), opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } func (suite *IntegrationTestSuite) TestRangeScanRangeCtxCancelBeforeResults() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) scan := RangeScan{ From: ScanTermMinimum(), To: ScanTermMaximum(), } ctx, cancel := context.WithCancel(context.Background()) cancel() opts := &ScanOptions{ IDsOnly: true, Context: ctx, } _, err := globalCollection.Scan(scan, opts) suite.Require().NotNil(err, err) } func (suite *IntegrationTestSuite) TestRangeScanRangeTimeoutBeforeResults() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) scan := RangeScan{ From: ScanTermMinimum(), To: ScanTermMaximum(), } opts := &ScanOptions{ IDsOnly: true, Timeout: 1 * time.Nanosecond, } _, err := globalCollection.Scan(scan, opts) suite.Require().NotNil(err, err) suite.Require().ErrorIs(err, ErrTimeout) } func (suite *IntegrationTestSuite) TestRangeScanRangeEmoji() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(RangeScanFeature) docIDs := map[string]struct{}{"\U0001F600": {}} value := makeBinaryValue(1) mutationState := suite.upsertAndCreateMutationState(globalCollection, docIDs, value, &UpsertOptions{ Expiry: 30 * time.Second, }) scan := NewRangeScanForPrefix("\U0001F600") opts := &ScanOptions{ IDsOnly: true, ConsistentWith: mutationState, } res, err := globalCollection.Scan(scan, opts) suite.Require().Nil(err, err) ids := make(map[string]struct{}) for { d := res.Next() if d == nil { break } suite.Assert().Zero(d.Cas()) var v interface{} err := d.Content(&v) suite.Assert().ErrorIs(err, ErrInvalidArgument) suite.Assert().Zero(d.ExpiryTime()) ids[d.ID()] = struct{}{} } suite.Assert().Len(ids, len(docIDs)) for id := range docIDs { suite.Assert().Contains(ids, id) } err = res.Err() suite.Require().Nil(err, err) numVbuckets := suite.numVbuckets() suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 1) suite.verifyRangeScanTracing(nilParents[0], numVbuckets, scan, globalScope.Name(), globalCollection.Name(), opts) suite.AssertKVMetrics(meterNameCBOperations, "range_scan", 1, false) } gocb-2.6.3/collection_subdoc.go000066400000000000000000000231711441755043100164650ustar00rootroot00000000000000package gocb import ( "context" "encoding/json" "errors" "time" "github.com/couchbase/gocbcore/v10/memd" "github.com/couchbase/gocbcore/v10" ) // LookupInOptions are the set of options available to LookupIn. type LookupInOptions struct { Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { DocFlags SubdocDocFlag User string } noMetrics bool } // LookupIn performs a set of subdocument lookup operations on the document identified by id. func (c *Collection) LookupIn(id string, ops []LookupInSpec, opts *LookupInOptions) (docOut *LookupInResult, errOut error) { if opts == nil { opts = &LookupInOptions{} } opm := c.newKvOpManager("lookup_in", opts.ParentSpan) defer opm.Finish(opts.noMetrics) opm.SetDocumentID(id) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) if err := opm.CheckReadyForOp(); err != nil { return nil, err } return c.internalLookupIn(opm, ops, memd.SubdocDocFlag(opts.Internal.DocFlags)) } func (c *Collection) internalLookupIn( opm *kvOpManager, ops []LookupInSpec, flags memd.SubdocDocFlag, ) (docOut *LookupInResult, errOut error) { var subdocs []gocbcore.SubDocOp for _, op := range ops { if op.op == memd.SubDocOpGet && op.path == "" { if op.isXattr { return nil, errors.New("invalid xattr fetch with no path") } subdocs = append(subdocs, gocbcore.SubDocOp{ Op: memd.SubDocOpGetDoc, Flags: memd.SubdocFlag(SubdocFlagNone), }) continue } else if op.op == memd.SubDocOpDictSet && op.path == "" { if op.isXattr { return nil, errors.New("invalid xattr set with no path") } subdocs = append(subdocs, gocbcore.SubDocOp{ Op: memd.SubDocOpSetDoc, Flags: memd.SubdocFlag(SubdocFlagNone), }) continue } flags := memd.SubdocFlagNone if op.isXattr { flags |= memd.SubdocFlagXattrPath } subdocs = append(subdocs, gocbcore.SubDocOp{ Op: op.op, Path: op.path, Flags: flags, }) } agent, err := c.getKvProvider() if err != nil { return nil, err } err = opm.Wait(agent.LookupIn(gocbcore.LookupInOptions{ Key: opm.DocumentID(), Ops: subdocs, CollectionName: opm.CollectionName(), ScopeName: opm.ScopeName(), RetryStrategy: opm.RetryStrategy(), TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), Flags: flags, User: opm.Impersonate(), }, func(res *gocbcore.LookupInResult, err error) { if err != nil && res == nil { errOut = opm.EnhanceErr(err) } if res != nil { docOut = &LookupInResult{} docOut.cas = Cas(res.Cas) docOut.contents = make([]lookupInPartial, len(subdocs)) for i, opRes := range res.Ops { docOut.contents[i].err = opm.EnhanceErr(opRes.Err) docOut.contents[i].data = json.RawMessage(opRes.Value) } } if err == nil { opm.Resolve(nil) } else { opm.Reject() } })) if err != nil { errOut = err } return } // StoreSemantics is used to define the document level action to take during a MutateIn operation. type StoreSemantics uint8 const ( // StoreSemanticsReplace signifies to Replace the document, and fail if it does not exist. // This is the default action StoreSemanticsReplace StoreSemantics = iota // StoreSemanticsUpsert signifies to replace the document or create it if it doesn't exist. StoreSemanticsUpsert // StoreSemanticsInsert signifies to create the document, and fail if it exists. StoreSemanticsInsert ) // MutateInOptions are the set of options available to MutateIn. type MutateInOptions struct { Expiry time.Duration Cas Cas PersistTo uint ReplicateTo uint DurabilityLevel DurabilityLevel StoreSemantic StoreSemantics Timeout time.Duration RetryStrategy RetryStrategy ParentSpan RequestSpan PreserveExpiry bool // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { DocFlags SubdocDocFlag User string } } // MutateIn performs a set of subdocument mutations on the document specified by id. func (c *Collection) MutateIn(id string, ops []MutateInSpec, opts *MutateInOptions) (mutOut *MutateInResult, errOut error) { if opts == nil { opts = &MutateInOptions{} } opm := c.newKvOpManager("mutate_in", opts.ParentSpan) defer opm.Finish(false) opm.SetDocumentID(id) opm.SetRetryStrategy(opts.RetryStrategy) opm.SetTimeout(opts.Timeout) opm.SetImpersonate(opts.Internal.User) opm.SetContext(opts.Context) opm.SetPreserveExpiry(opts.PreserveExpiry) opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel) if err := opm.CheckReadyForOp(); err != nil { return nil, err } return c.internalMutateIn(opm, opts.StoreSemantic, opts.Expiry, opts.Cas, ops, memd.SubdocDocFlag(opts.Internal.DocFlags)) } func jsonMarshalMultiArray(in interface{}) ([]byte, error) { out, err := json.Marshal(in) if err != nil { return nil, err } // Assert first character is a '[' if len(out) < 2 || out[0] != '[' { return nil, makeInvalidArgumentsError("not a JSON array") } out = out[1 : len(out)-1] return out, nil } func jsonMarshalMutateSpec(op MutateInSpec) ([]byte, memd.SubdocFlag, error) { if op.value == nil { // If the mutation is to write, then this is a json `null` value switch op.op { case memd.SubDocOpDictAdd, memd.SubDocOpDictSet, memd.SubDocOpReplace, memd.SubDocOpArrayPushLast, memd.SubDocOpArrayPushFirst, memd.SubDocOpArrayInsert, memd.SubDocOpArrayAddUnique, memd.SubDocOpSetDoc, memd.SubDocOpAddDoc: return []byte("null"), memd.SubdocFlagNone, nil } return nil, memd.SubdocFlagNone, nil } if macro, ok := op.value.(MutationMacro); ok { return []byte(macro), memd.SubdocFlagExpandMacros | memd.SubdocFlagXattrPath, nil } if op.multiValue { bytes, err := jsonMarshalMultiArray(op.value) return bytes, memd.SubdocFlagNone, err } bytes, err := json.Marshal(op.value) return bytes, memd.SubdocFlagNone, err } func (c *Collection) internalMutateIn( opm *kvOpManager, action StoreSemantics, expiry time.Duration, cas Cas, ops []MutateInSpec, docFlags memd.SubdocDocFlag, ) (mutOut *MutateInResult, errOut error) { preserveTTL := opm.PreserveExpiry() if action == StoreSemanticsReplace { // this is the default behaviour if expiry > 0 && preserveTTL { return nil, makeInvalidArgumentsError("cannot use preserve ttl with expiry for replace store semantics") } } else if action == StoreSemanticsUpsert { docFlags |= memd.SubdocDocFlagMkDoc } else if action == StoreSemanticsInsert { if preserveTTL { return nil, makeInvalidArgumentsError("cannot use preserve ttl with insert store semantics") } docFlags |= memd.SubdocDocFlagAddDoc } else { return nil, makeInvalidArgumentsError("invalid StoreSemantics value provided") } var subdocs []gocbcore.SubDocOp for _, op := range ops { if op.path == "" { switch op.op { case memd.SubDocOpDictAdd: return nil, makeInvalidArgumentsError("cannot specify a blank path with InsertSpec") case memd.SubDocOpDictSet: return nil, makeInvalidArgumentsError("cannot specify a blank path with UpsertSpec") case memd.SubDocOpDelete: op.op = memd.SubDocOpDeleteDoc case memd.SubDocOpReplace: op.op = memd.SubDocOpSetDoc default: } } etrace := c.startKvOpTrace("request_encoding", opm.TraceSpanContext(), true) bytes, flags, err := jsonMarshalMutateSpec(op) etrace.End() if err != nil { return nil, err } if op.createPath { flags |= memd.SubdocFlagMkDirP } if op.isXattr { flags |= memd.SubdocFlagXattrPath } subdocs = append(subdocs, gocbcore.SubDocOp{ Op: op.op, Flags: flags, Path: op.path, Value: bytes, }) } agent, err := c.getKvProvider() if err != nil { return nil, err } err = opm.Wait(agent.MutateIn(gocbcore.MutateInOptions{ Key: opm.DocumentID(), Flags: docFlags, Cas: gocbcore.Cas(cas), Ops: subdocs, Expiry: durationToExpiry(expiry), CollectionName: opm.CollectionName(), ScopeName: opm.ScopeName(), DurabilityLevel: opm.DurabilityLevel(), DurabilityLevelTimeout: opm.DurabilityTimeout(), RetryStrategy: opm.RetryStrategy(), TraceContext: opm.TraceSpanContext(), Deadline: opm.Deadline(), User: opm.Impersonate(), PreserveExpiry: preserveTTL, }, func(res *gocbcore.MutateInResult, err error) { if err != nil { // GOCBC-1019: Due to a previous bug in gocbcore we need to convert cas mismatch back to exists. if kvErr, ok := err.(*gocbcore.KeyValueError); ok { if errors.Is(kvErr.InnerError, ErrCasMismatch) { kvErr.InnerError = ErrDocumentExists } } errOut = opm.EnhanceErr(err) opm.Reject() return } mutOut = &MutateInResult{} mutOut.cas = Cas(res.Cas) mutOut.mt = opm.EnhanceMt(res.MutationToken) mutOut.contents = make([]mutateInPartial, len(res.Ops)) for i, op := range res.Ops { mutOut.contents[i] = mutateInPartial{data: op.Value} } opm.Resolve(mutOut.mt) })) if err != nil { errOut = err } return } gocb-2.6.3/collection_subdoc_test.go000066400000000000000000000570111441755043100175240ustar00rootroot00000000000000package gocb import ( "errors" "strings" "time" "github.com/couchbase/gocbcore/v10/memd" ) func (suite *IntegrationTestSuite) TestInsertLookupIn() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) docId := generateDocId("lookupDoc") type beerWithCountable struct { testBeerDocument Countable []string `json:"countable"` } var doc beerWithCountable err := loadJSONTestDataset("beer_sample_single", &doc.testBeerDocument) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } doc.Countable = []string{"one", "two"} mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("name", nil), GetSpec("description", nil), ExistsSpec("doesnt", nil), ExistsSpec("style", nil), GetSpec("doesntexist", nil), CountSpec("countable", nil), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } if result.Exists(2) { suite.T().Fatalf("Expected doesnt field to not exist") } if !result.Exists(3) { suite.T().Fatalf("Expected style field to exist") } var name string err = result.ContentAt(0, &name) if err != nil { suite.T().Fatalf("Failed to get name from LookupInResult, %v", err) } if name != doc.Name { suite.T().Fatalf("Expected name to be %s but was %s", doc.Name, name) } var desc string err = result.ContentAt(1, &desc) if err != nil { suite.T().Fatalf("Failed to get description from LookupInResult, %v", err) } if desc != doc.Description { suite.T().Fatalf("Expected description to be %s but was %s", doc.Description, desc) } var idontexist string err = result.ContentAt(4, &idontexist) if err == nil { suite.T().Fatalf("Expected lookup on a non existent field to return error") } if !errors.Is(err, ErrPathNotFound) { suite.T().Fatalf("Expected error to be path not found but was %+v", err) } var count int err = result.ContentAt(5, &count) if err != nil { suite.T().Fatalf("Failed to get count from LookupInResult, %v", err) } if count != 2 { suite.T().Fatalf("LookupIn Result count should have be 2 but was %d", count) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 2) suite.AssertKvOpSpan(nilParents[0], "insert", memd.CmdAdd.Name(), true, DurabilityLevelNone) suite.AssertKvOpSpan(nilParents[1], "lookup_in", memd.CmdSubDocMultiLookup.Name(), false, DurabilityLevelNone) } func (suite *IntegrationTestSuite) TestMutateInBasicCrud() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("mutateIn", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error: %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } fishName := "blobfish" newName := "fishy beer" newStyle := "fishy" subRes, err := globalCollection.MutateIn("mutateIn", []MutateInSpec{ InsertSpec("fish", fishName, nil), UpsertSpec("name", newName, nil), UpsertSpec("newName", newName, nil), UpsertSpec("description", nil, nil), ReplaceSpec("style", newStyle, nil), ReplaceSpec("category", nil, nil), RemoveSpec("type", nil), InsertSpec("x.foo.bar", "ddd", &InsertSpecOptions{CreatePath: true}), UpsertSpec("x.foo.barbar", "barbar", &UpsertSpecOptions{}), ReplaceSpec("x.foo.bar", "eee", &ReplaceSpecOptions{}), }, nil) if err != nil { suite.T().Fatalf("MutateIn failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } getRes, err := globalCollection.Get("mutateIn", nil) if err != nil { suite.T().Fatalf("Getting document errored: %v", err) } type fishBeerDocument struct { testBeerDocument NewName string `json:"newName"` Fish string `json:"fish"` } var actualDoc fishBeerDocument err = getRes.Content(&actualDoc) if err != nil { suite.T().Fatalf("Getting content errored: %v", err) } rawMap := make(map[string]interface{}) err = getRes.Content(&rawMap) if err != nil { suite.T().Fatalf("Getting content to raw map errored: %v", err) } if rawMap["brewery_id"] != doc.BreweryID { suite.T().Fatalf("raw map content did not match, expected %#v but was %#v", doc.BreweryID, rawMap["brewery_id"]) } if rawMap["category"] != nil { suite.T().Fatalf("raw map content did not match, expected %#v but was %#v", nil, rawMap["category"]) } if rawMap["description"] != nil { suite.T().Fatalf("raw map content did not match, expected %#v but was %#v", nil, rawMap["description"]) } expectedDoc := fishBeerDocument{ testBeerDocument: doc, NewName: newName, Fish: fishName, } expectedDoc.Name = newName expectedDoc.Style = newStyle expectedDoc.Type = "" expectedDoc.Category = "" expectedDoc.Description = "" if actualDoc != expectedDoc { suite.T().Fatalf("results did not match, expected %#v but was %#v", expectedDoc, actualDoc) } suite.Require().Contains(globalTracer.GetSpans(), nil) nilParents := globalTracer.GetSpans()[nil] suite.Require().Equal(len(nilParents), 3) suite.AssertKvOpSpan(nilParents[0], "upsert", memd.CmdSet.Name(), true, DurabilityLevelNone) suite.AssertKvSpan(nilParents[1], "mutate_in", DurabilityLevelNone) suite.AssertEncodingSpansEq(nilParents[1].Spans, 10) suite.AssertCmdSpans(nilParents[1].Spans, memd.CmdSubDocMultiMutation.Name()) suite.AssertKvOpSpan(nilParents[2], "get", memd.CmdGet.Name(), false, DurabilityLevelNone) } func (suite *IntegrationTestSuite) TestMutateInBasicArray() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) docId := generateDocId("mutateInArray") doc := struct { }{} mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error: %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } subRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ ArrayAppendSpec("array", "clownfish", &ArrayAppendSpecOptions{ CreatePath: true, }), ArrayPrependSpec("array", "whaleshark", nil), ArrayInsertSpec("array[1]", "catfish", nil), ArrayAppendSpec("array", []string{"manta ray", "stingray"}, &ArrayAppendSpecOptions{HasMultiple: true}), ArrayPrependSpec("array", []string{"carp", "goldfish"}, &ArrayPrependSpecOptions{HasMultiple: true}), ArrayInsertSpec("array[1]", []string{"eel", "stonefish"}, &ArrayInsertSpecOptions{HasMultiple: true}), }, nil) if err != nil { suite.T().Fatalf("MutateIn failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } getRes, err := globalCollection.Get(docId, nil) if err != nil { suite.T().Fatalf("Getting document errored: %v", err) } type fishBeerDocument struct { Fish []string `json:"array"` } var actualDoc fishBeerDocument err = getRes.Content(&actualDoc) if err != nil { suite.T().Fatalf("Getting content errored: %v", err) } expectedDoc := fishBeerDocument{ Fish: []string{"carp", "eel", "stonefish", "goldfish", "whaleshark", "catfish", "clownfish", "manta ray", "stingray"}, } if len(expectedDoc.Fish) != len(actualDoc.Fish) { suite.T().Fatalf("results did not match, expected %v but was %v", expectedDoc, actualDoc) } for i, fish := range expectedDoc.Fish { if fish != actualDoc.Fish[i] { suite.T().Fatalf("results did not match, expected %s at index %d but was %s", fish, i, actualDoc.Fish[i]) } } } func (suite *IntegrationTestSuite) TestInsertLookupInInsertGetFull() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) suite.skipIfUnsupported(XattrFeature) docId := generateDocId("lookupDocGetFull") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } subRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ InsertSpec("xattrpath", "xattrvalue", &InsertSpecOptions{IsXattr: true}), ReplaceSpec("", doc, nil), }, &MutateInOptions{StoreSemantic: StoreSemanticsUpsert, Expiry: 20 * time.Second}) if err != nil { suite.T().Fatalf("MutateIn failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("MutateIn CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("$document.exptime", &GetSpecOptions{IsXattr: true}), GetSpec("", nil), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var exptime int err = result.ContentAt(0, &exptime) if err != nil { suite.T().Fatalf("Failed to get expiry from LookupInResult, %v", err) } if exptime == 0 { suite.T().Fatalf("Expected expiry to be non zero") } var actualDoc testBeerDocument err = result.ContentAt(1, &actualDoc) if err != nil { suite.T().Fatalf("Failed to get name from LookupInResult, %v", err) } if actualDoc != doc { suite.T().Fatalf("Expected doc to be %v but was %v", doc, actualDoc) } } func (suite *IntegrationTestSuite) TestMutateInLookupInCounters() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) docId := generateDocId("mutateInLookupInCounters") doc := struct { Counter int `json:"counter"` }{ Counter: 20, } mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error: %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } subRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ IncrementSpec("counter", 10, nil), DecrementSpec("counter", 5, nil), }, nil) if err != nil { suite.T().Fatalf("Increment failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("counter", nil), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var counter int err = result.ContentAt(0, &counter) if err != nil { suite.T().Fatalf("Failed to get counter from LookupInResult, %v", err) } if counter != 25 { suite.T().Fatalf("Expected counter to be 25 but was %d", counter) } } func (suite *IntegrationTestSuite) TestMutateInLookupInMacro() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) suite.skipIfUnsupported(ExpandMacrosFeature) docId := generateDocId("mutateInInsertMacro") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Insert(docId, doc, nil) if err != nil { suite.T().Fatalf("Insert failed, error: %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } subRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ InsertSpec("caspath", MutationMacroCAS, nil), }, nil) if err != nil { suite.T().Fatalf("MutateIn failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("MutateIn CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("caspath", &GetSpecOptions{IsXattr: true}), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var caspath string err = result.ContentAt(0, &caspath) if err != nil { suite.T().Fatalf("Failed to get caspath from LookupInResult, %v", err) } if !strings.HasPrefix(caspath, "0x") { suite.T().Fatalf("Expected caspath to start with 0x but was %s", caspath) } } func (suite *IntegrationTestSuite) TestSubdocNil() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) _, err := globalCollection.Upsert("nullvalues", struct{}{}, nil) suite.Require().Nil(err, err) mutateOps := []MutateInSpec{ InsertSpec("insert", nil, nil), UpsertSpec("upsert", nil, nil), ReplaceSpec("upsert", nil, nil), ArrayAppendSpec("array", nil, &ArrayAppendSpecOptions{ CreatePath: true, }), ArrayPrependSpec("array", []interface{}{nil, nil, nil}, &ArrayPrependSpecOptions{ HasMultiple: true, }), ArrayInsertSpec("array[1]", nil, nil), } mutRes, err := globalCollection.MutateIn("nullvalues", mutateOps, nil) suite.Require().Nil(err, err) suite.Assert().NotZero(mutRes.Cas()) lookupOps := []LookupInSpec{ GetSpec("insert", nil), GetSpec("upsert", nil), GetSpec("array", nil), } lookupRes, err := globalCollection.LookupIn("nullvalues", lookupOps, nil) suite.Require().Nil(err, err) suite.Assert().NotZero(lookupRes.Cas()) var insertVal interface{} if suite.Assert().Nil(lookupRes.ContentAt(0, &insertVal)) { suite.Assert().Nil(insertVal) } var upsertVal interface{} if suite.Assert().Nil(lookupRes.ContentAt(1, &upsertVal)) { suite.Assert().Nil(upsertVal) } var arrayVal []interface{} if suite.Assert().Nil(lookupRes.ContentAt(2, &arrayVal)) { if suite.Assert().Len(arrayVal, 5) { suite.Assert().Nil(arrayVal[0], nil) suite.Assert().Nil(arrayVal[1], nil) suite.Assert().Nil(arrayVal[2], nil) suite.Assert().Nil(arrayVal[3], nil) suite.Assert().Nil(arrayVal[4], nil) } } mutateOps = []MutateInSpec{ ReplaceSpec("", nil, nil), } mutRes, err = globalCollection.MutateIn("nullvalues", mutateOps, &MutateInOptions{}) suite.Require().Nil(err, err) lookupOps = []LookupInSpec{ GetSpec("", nil), } lookupRes, err = globalCollection.LookupIn("nullvalues", lookupOps, nil) suite.Require().Nil(err, err) var doc interface{} if suite.Assert().Nil(lookupRes.ContentAt(0, &doc)) { suite.Assert().Nil(doc) } } func (suite *IntegrationTestSuite) TestMutateInBlankPathRemove() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) suite.skipIfUnsupported(XattrFeature) doc := struct { Thing string `json:"thing"` }{ Thing: "from the depths", } mutRes, err := globalCollection.Upsert("mutateInBlankPathRemove", doc, nil) suite.Require().Nil(err) suite.Assert().NotZero(mutRes.Cas()) subRes, err := globalCollection.MutateIn("mutateInBlankPathRemove", []MutateInSpec{ RemoveSpec("", nil), }, nil) suite.Require().Nil(err) suite.Assert().NotZero(subRes.Cas()) _, err = globalCollection.Get("mutateInBlankPathRemove", nil) if !errors.Is(err, ErrDocumentNotFound) { suite.T().Fatalf("Expected error to be doc not found but was %v", err) } } func (suite *IntegrationTestSuite) TestPreserveExpiryMutateIn() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(XattrFeature) suite.skipIfUnsupported(PreserveExpiryFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) suite.Require().Nil(err, err) start := time.Now() mutRes, err := globalCollection.Upsert("preservettlmutatein", doc, &UpsertOptions{Expiry: 25 * time.Second}) suite.Require().Nil(err, err) suite.Assert().NotZero(mutRes.Cas()) mutInRes, err := globalCollection.MutateIn("preservettlmutatein", []MutateInSpec{ UpsertSpec("test", "test", nil), }, &MutateInOptions{PreserveExpiry: true}) suite.Require().Nil(err, err) suite.Assert().NotZero(mutInRes.Cas()) mutatedDoc, err := globalCollection.Get("preservettlmutatein", &GetOptions{WithExpiry: true}) suite.Require().Nil(err, err) suite.Assert().InDelta(start.Add(25*time.Second).Unix(), mutatedDoc.ExpiryTime().Unix(), 5) _, err = globalCollection.MutateIn("preservettlmutatein", []MutateInSpec{ UpsertSpec("test", "test", nil), }, &MutateInOptions{PreserveExpiry: true, StoreSemantic: StoreSemanticsInsert}) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected invalid args error but was %v", err) } _, err = globalCollection.MutateIn("preservettlmutatein", []MutateInSpec{ UpsertSpec("test", "test", nil), }, &MutateInOptions{PreserveExpiry: true, Expiry: 5 * time.Second}) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("Expected invalid args error but was %v", err) } } // GOCBC-1019: Due to a previous bug in gocbcore we need to convert cas mismatch back to exists. func (suite *IntegrationTestSuite) TestCasMismatchConvertedToExists() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) docId := generateDocId("subdocCasMismatchDoc") var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Insert(docId, doc, &InsertOptions{Expiry: 10 * time.Second}) suite.Require().Nil(err, err) if mutRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } _, err = globalCollection.MutateIn(docId, []MutateInSpec{ ReplaceSpec("brewery_id", "test", nil), }, &MutateInOptions{ Cas: Cas(123), }) if !errors.Is(err, ErrDocumentExists) { suite.T().Fatalf("Expected error to be exists but was: %v", err) } } func (suite *IntegrationTestSuite) TestMutateInLookupInXattrs() { type beerWithCountable struct { testBeerDocument Countable []string `json:"countable"` } var doc beerWithCountable err := loadJSONTestDataset("beer_sample_single", &doc.testBeerDocument) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } docId := generateDocId("lookupXattrDoc") _, err = globalCollection.Upsert(docId, doc, nil) suite.Require().Nil(err, err) doc.Countable = []string{"one", "two"} mutRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ InsertSpec("x.name", doc.Name, &InsertSpecOptions{IsXattr: true, CreatePath: true}), InsertSpec("x.description", "ddd", &InsertSpecOptions{IsXattr: true}), UpsertSpec("x.style", doc.Style, &UpsertSpecOptions{IsXattr: true}), UpsertSpec("x.countable", doc.Countable, &UpsertSpecOptions{IsXattr: true}), ReplaceSpec("x.description", doc.Description, &ReplaceSpecOptions{IsXattr: true}), InsertSpec("x.foo.bar", "ddd", &InsertSpecOptions{IsXattr: true, CreatePath: true}), UpsertSpec("x.foo.barbar", "barbar", &UpsertSpecOptions{IsXattr: true}), ReplaceSpec("x.foo.bar", "eee", &ReplaceSpecOptions{IsXattr: true}), }, nil) if err != nil { suite.T().Fatalf("MutateIn failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("MutateIn CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("x.name", &GetSpecOptions{IsXattr: true}), GetSpec("x.description", &GetSpecOptions{IsXattr: true}), ExistsSpec("x.doesnt", &ExistsSpecOptions{IsXattr: true}), ExistsSpec("x.style", &ExistsSpecOptions{IsXattr: true}), GetSpec("x.doesntexist", &GetSpecOptions{IsXattr: true}), CountSpec("x.countable", &CountSpecOptions{IsXattr: true}), GetSpec("x.foo.bar", &GetSpecOptions{IsXattr: true}), GetSpec("x.foo.barbar", &GetSpecOptions{IsXattr: true}), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } if result.Exists(2) { suite.T().Fatalf("Expected doesnt field to not exist") } if !result.Exists(3) { suite.T().Fatalf("Expected style field to exist") } var name string err = result.ContentAt(0, &name) if err != nil { suite.T().Fatalf("Failed to get name from LookupInResult, %v", err) } if name != doc.Name { suite.T().Fatalf("Expected name to be %s but was %s", doc.Name, name) } var desc string err = result.ContentAt(1, &desc) if err != nil { suite.T().Fatalf("Failed to get description from LookupInResult, %v", err) } if desc != doc.Description { suite.T().Fatalf("Expected description to be %s but was %s", doc.Description, desc) } var idontexist string err = result.ContentAt(4, &idontexist) if err == nil { suite.T().Fatalf("Expected lookup on a non existent field to return error") } if !errors.Is(err, ErrPathNotFound) { suite.T().Fatalf("Expected error to be path not found but was %+v", err) } var count int err = result.ContentAt(5, &count) if err != nil { suite.T().Fatalf("Failed to get count from LookupInResult, %v", err) } if count != 2 { suite.T().Fatalf("LookupIn Result count should have be 2 but was %d", count) } var bar string err = result.ContentAt(6, &bar) if err != nil { suite.T().Fatalf("Failed to get description from LookupInResult, %v", err) } if bar != "eee" { suite.T().Fatalf("Expected description to be %s but was %s", doc.Description, desc) } var barbar string err = result.ContentAt(7, &barbar) if err != nil { suite.T().Fatalf("Failed to get description from LookupInResult, %v", err) } if barbar != "barbar" { suite.T().Fatalf("Expected description to be %s but was %s", doc.Description, desc) } } func (suite *IntegrationTestSuite) TestMutateInBasicArrayXattrs() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) docId := generateDocId("mutateInArrayXattr") _, err := globalCollection.Upsert(docId, "{}", nil) suite.Require().Nil(err, err) subRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ ArrayAppendSpec("array", "clownfish", &ArrayAppendSpecOptions{IsXattr: true, CreatePath: true}), ArrayPrependSpec("array", "whaleshark", &ArrayPrependSpecOptions{IsXattr: true}), ArrayInsertSpec("array[1]", "catfish", &ArrayInsertSpecOptions{IsXattr: true}), ArrayAppendSpec("array", []string{"manta ray", "stingray"}, &ArrayAppendSpecOptions{IsXattr: true, HasMultiple: true}), ArrayPrependSpec("array", []string{"carp", "goldfish"}, &ArrayPrependSpecOptions{IsXattr: true, HasMultiple: true}), ArrayInsertSpec("array[1]", []string{"eel", "stonefish"}, &ArrayInsertSpecOptions{IsXattr: true, HasMultiple: true}), }, nil) if err != nil { suite.T().Fatalf("MutateIn failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("array", &GetSpecOptions{IsXattr: true}), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var actualDoc []string err = result.ContentAt(0, &actualDoc) if err != nil { suite.T().Fatalf("Failed to get actualDoc from LookupInResult, %v", err) } expectedDoc := []string{"carp", "eel", "stonefish", "goldfish", "whaleshark", "catfish", "clownfish", "manta ray", "stingray"} if len(expectedDoc) != len(actualDoc) { suite.T().Fatalf("results did not match, expected %v but was %v", expectedDoc, actualDoc) } for i, fish := range expectedDoc { if fish != actualDoc[i] { suite.T().Fatalf("results did not match, expected %s at index %d but was %s", fish, i, actualDoc[i]) } } } func (suite *IntegrationTestSuite) TestMutateInLookupInCountersXattrs() { suite.skipIfUnsupported(KeyValueFeature) suite.skipIfUnsupported(SubdocFeature) docId := generateDocId("mutateInLookupInCountersXattrs") _, err := globalCollection.Upsert(docId, "{}", nil) suite.Require().Nil(err, err) subRes, err := globalCollection.MutateIn(docId, []MutateInSpec{ InsertSpec("count", 10, &InsertSpecOptions{IsXattr: true}), }, nil) if err != nil { suite.T().Fatalf("Increment failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } subRes, err = globalCollection.MutateIn(docId, []MutateInSpec{ DecrementSpec("count", 3, &CounterSpecOptions{IsXattr: true}), }, nil) if err != nil { suite.T().Fatalf("Increment failed, error was %v", err) } if subRes.Cas() == 0 { suite.T().Fatalf("Insert CAS was 0") } result, err := globalCollection.LookupIn(docId, []LookupInSpec{ GetSpec("count", &GetSpecOptions{ IsXattr: true, }), }, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var counter int err = result.ContentAt(0, &counter) if err != nil { suite.T().Fatalf("Failed to get counter from LookupInResult, %v", err) } if counter != 7 { suite.T().Fatalf("Expected counter to be 25 but was %v", counter) } } gocb-2.6.3/collection_test.go000066400000000000000000000017701441755043100161660ustar00rootroot00000000000000package gocb func (suite *UnitTestSuite) TestCollectionName() { bName := "bucket" sName := "scope" cName := "collection" b := suite.bucket(bName, suite.defaultTimeoutConfig(), nil) s := b.Scope(sName) c := s.Collection(cName) suite.Assert().Equal(bName, c.bucketName()) suite.Assert().Equal(sName, c.ScopeName()) suite.Assert().Equal(cName, c.Name()) } func (suite *UnitTestSuite) TestDefaultScopeCollectionName() { bName := "bucket" cName := "collection" b := suite.bucket(bName, suite.defaultTimeoutConfig(), nil) c := b.Collection(cName) suite.Assert().Equal(bName, c.bucketName()) suite.Assert().Equal("_default", c.ScopeName()) suite.Assert().Equal(cName, c.Name()) } func (suite *UnitTestSuite) TestDefaultScopeDefaultCollectionName() { bName := "bucket" b := suite.bucket(bName, suite.defaultTimeoutConfig(), nil) c := b.DefaultCollection() suite.Assert().Equal(bName, c.bucketName()) suite.Assert().Equal("_default", c.ScopeName()) suite.Assert().Equal("_default", c.Name()) } gocb-2.6.3/config_profile.go000066400000000000000000000026141441755043100157570ustar00rootroot00000000000000package gocb import "time" var developmentProfile = ClusterOptions{ TimeoutsConfig: TimeoutsConfig{ KVTimeout: 20 * time.Second, ConnectTimeout: 20 * time.Second, KVDurableTimeout: 20 * time.Second, KVScanTimeout: 20 * time.Second, ViewTimeout: 120 * time.Second, AnalyticsTimeout: 120 * time.Second, SearchTimeout: 120 * time.Second, ManagementTimeout: 120 * time.Second, QueryTimeout: 120 * time.Second, }, } // ClusterConfigProfile represents a named profile that can be applied to ClusterOptions. // VOLATILE: This API is subject to change at any time. type ClusterConfigProfile string const ( // ClusterConfigProfileWanDevelopment represents a wan development profile that can be applied to the ClusterOptions // overwriting any properties that exist on the profile. // VOLATILE: This API is subject to change at any time. ClusterConfigProfileWanDevelopment ClusterConfigProfile = "wan-development" ) // ApplyProfile will apply a named profile to the ClusterOptions overwriting any properties that // exist on the profile. // VOLATILE: This API is subject to change at any time. func (opts *ClusterOptions) ApplyProfile(profile ClusterConfigProfile) error { if profile == ClusterConfigProfileWanDevelopment { opts.TimeoutsConfig = developmentProfile.TimeoutsConfig return nil } return makeInvalidArgumentsError("unknown configuration profile") } gocb-2.6.3/config_profile_test.go000066400000000000000000000036411441755043100170170ustar00rootroot00000000000000package gocb import "time" var defaultConfig = ClusterOptions{ TimeoutsConfig: TimeoutsConfig{ KVTimeout: 2500 * time.Millisecond, ConnectTimeout: 10 * time.Second, KVDurableTimeout: 10 * time.Second, KVScanTimeout: 10 * time.Second, ViewTimeout: 75 * time.Second, AnalyticsTimeout: 75 * time.Second, SearchTimeout: 75 * time.Second, ManagementTimeout: 75 * time.Second, QueryTimeout: 75 * time.Second, }, Transcoder: NewJSONTranscoder(), Tracer: NewThresholdLoggingTracer(nil), Meter: newAggregatingMeter(nil), RetryStrategy: NewBestEffortRetryStrategy(nil), } func (suite *UnitTestSuite) TestDevelopmentConfigProfile() { auth := PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, } options := defaultConfig options.Authenticator = auth err := options.ApplyProfile(ClusterConfigProfileWanDevelopment) suite.Require().Nil(err) suite.Assert().Equal(20*time.Second, options.TimeoutsConfig.KVTimeout) suite.Assert().Equal(20*time.Second, options.TimeoutsConfig.ConnectTimeout) suite.Assert().Equal(20*time.Second, options.TimeoutsConfig.KVDurableTimeout) suite.Assert().Equal(20*time.Second, options.TimeoutsConfig.KVScanTimeout) suite.Assert().Equal(120*time.Second, options.TimeoutsConfig.ViewTimeout) suite.Assert().Equal(120*time.Second, options.TimeoutsConfig.AnalyticsTimeout) suite.Assert().Equal(120*time.Second, options.TimeoutsConfig.SearchTimeout) suite.Assert().Equal(120*time.Second, options.TimeoutsConfig.ManagementTimeout) suite.Assert().Equal(120*time.Second, options.TimeoutsConfig.QueryTimeout) } func (suite *UnitTestSuite) TestUnknownConfigProfile() { auth := PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, } options := defaultConfig options.Authenticator = auth err := options.ApplyProfile("unknown") suite.Require().ErrorIs(err, ErrInvalidArgument) } gocb-2.6.3/constants.go000066400000000000000000000313561441755043100150130ustar00rootroot00000000000000package gocb import ( "fmt" "time" gocbcore "github.com/couchbase/gocbcore/v10" "github.com/couchbase/gocbcore/v10/memd" ) const ( goCbVersionStr = "v2.6.3" durabilityTimeoutFloor = 1500 * time.Millisecond ) // QueryIndexType provides information on the type of indexer used for an index. type QueryIndexType string const ( // QueryIndexTypeGsi indicates that GSI was used to build the index. QueryIndexTypeGsi QueryIndexType = "gsi" // QueryIndexTypeView indicates that views were used to build the index. QueryIndexTypeView QueryIndexType = "views" ) // QueryStatus provides information about the current status of a query. type QueryStatus string const ( // QueryStatusRunning indicates the query is still running QueryStatusRunning QueryStatus = "running" // QueryStatusSuccess indicates the query was successful. QueryStatusSuccess QueryStatus = "success" // QueryStatusErrors indicates a query completed with errors. QueryStatusErrors QueryStatus = "errors" // QueryStatusCompleted indicates a query has completed. QueryStatusCompleted QueryStatus = "completed" // QueryStatusStopped indicates a query has been stopped. QueryStatusStopped QueryStatus = "stopped" // QueryStatusTimeout indicates a query timed out. QueryStatusTimeout QueryStatus = "timeout" // QueryStatusClosed indicates that a query was closed. QueryStatusClosed QueryStatus = "closed" // QueryStatusFatal indicates that a query ended with a fatal error. QueryStatusFatal QueryStatus = "fatal" // QueryStatusAborted indicates that a query was aborted. QueryStatusAborted QueryStatus = "aborted" // QueryStatusUnknown indicates that the query status is unknown. QueryStatusUnknown QueryStatus = "unknown" ) // ServiceType specifies a particular Couchbase service type. type ServiceType gocbcore.ServiceType const ( // ServiceTypeManagement represents a management service. ServiceTypeManagement ServiceType = ServiceType(gocbcore.MgmtService) // ServiceTypeKeyValue represents a memcached service. ServiceTypeKeyValue ServiceType = ServiceType(gocbcore.MemdService) // ServiceTypeViews represents a views service. ServiceTypeViews ServiceType = ServiceType(gocbcore.CapiService) // ServiceTypeQuery represents a query service. ServiceTypeQuery ServiceType = ServiceType(gocbcore.N1qlService) // ServiceTypeSearch represents a full-text-search service. ServiceTypeSearch ServiceType = ServiceType(gocbcore.FtsService) // ServiceTypeAnalytics represents an analytics service. ServiceTypeAnalytics ServiceType = ServiceType(gocbcore.CbasService) // ServiceTypeEventing represents an eventing service. ServiceTypeEventing ServiceType = ServiceType(gocbcore.EventingService) ) // QueryProfileMode specifies the profiling mode to use during a query. type QueryProfileMode string const ( // QueryProfileModeNone disables query profiling QueryProfileModeNone QueryProfileMode = "off" // QueryProfileModePhases includes phase profiling information in the query response QueryProfileModePhases QueryProfileMode = "phases" // QueryProfileModeTimings includes timing profiling information in the query response QueryProfileModeTimings QueryProfileMode = "timings" ) // SubdocFlag provides special handling flags for sub-document operations type SubdocFlag memd.SubdocFlag const ( // SubdocFlagNone indicates no special behaviours SubdocFlagNone SubdocFlag = SubdocFlag(memd.SubdocFlagNone) // SubdocFlagCreatePath indicates you wish to recursively create the tree of paths // if it does not already exist within the document. SubdocFlagCreatePath SubdocFlag = SubdocFlag(memd.SubdocFlagMkDirP) // SubdocFlagXattr indicates your path refers to an extended attribute rather than the document. SubdocFlagXattr SubdocFlag = SubdocFlag(memd.SubdocFlagXattrPath) // SubdocFlagUseMacros indicates that you wish macro substitution to occur on the value SubdocFlagUseMacros SubdocFlag = SubdocFlag(memd.SubdocFlagExpandMacros) ) // SubdocDocFlag specifies document-level flags for a sub-document operation. type SubdocDocFlag memd.SubdocDocFlag const ( // SubdocDocFlagNone indicates no special behaviours SubdocDocFlagNone SubdocDocFlag = SubdocDocFlag(memd.SubdocDocFlagNone) // SubdocDocFlagMkDoc indicates that the document should be created if it does not already exist. SubdocDocFlagMkDoc SubdocDocFlag = SubdocDocFlag(memd.SubdocDocFlagMkDoc) // SubdocDocFlagAddDoc indices that the document should be created only if it does not already exist. SubdocDocFlagAddDoc SubdocDocFlag = SubdocDocFlag(memd.SubdocDocFlagAddDoc) // SubdocDocFlagAccessDeleted indicates that you wish to receive soft-deleted documents. // Internal: This should never be used and is not supported. SubdocDocFlagAccessDeleted SubdocDocFlag = SubdocDocFlag(memd.SubdocDocFlagAccessDeleted) // SubdocDocFlagCreateAsDeleted indicates that you wish to create a document in deleted state. // Internal: This should never be used and is not supported. SubdocDocFlagCreateAsDeleted SubdocDocFlag = SubdocDocFlag(memd.SubdocDocFlagCreateAsDeleted) ) // DurabilityLevel specifies the level of synchronous replication to use. type DurabilityLevel uint8 const ( // DurabilityLevelUnknown specifies that the durability level is not set and will default to the default durability level. DurabilityLevelUnknown DurabilityLevel = iota // DurabilityLevelNone specifies that no durability level should be applied. DurabilityLevelNone // DurabilityLevelMajority specifies that a mutation must be replicated (held in memory) to a majority of nodes. DurabilityLevelMajority // DurabilityLevelMajorityAndPersistOnMaster specifies that a mutation must be replicated (held in memory) to a // majority of nodes and also persisted (written to disk) on the active node. DurabilityLevelMajorityAndPersistOnMaster // DurabilityLevelPersistToMajority specifies that a mutation must be persisted (written to disk) to a majority // of nodes. DurabilityLevelPersistToMajority ) func (dl DurabilityLevel) toManagementAPI() (string, error) { switch dl { case DurabilityLevelNone: return "none", nil case DurabilityLevelMajority: return "majority", nil case DurabilityLevelMajorityAndPersistOnMaster: return "majorityAndPersistActive", nil case DurabilityLevelPersistToMajority: return "persistToMajority", nil default: return "", invalidArgumentsError{ message: fmt.Sprintf("unknown durability level: %d", dl), } } } func (dl DurabilityLevel) toMemd() (memd.DurabilityLevel, error) { switch dl { case DurabilityLevelNone: return memd.DurabilityLevel(0), nil case DurabilityLevelMajority: return memd.DurabilityLevelMajority, nil case DurabilityLevelMajorityAndPersistOnMaster: return memd.DurabilityLevelMajorityAndPersistOnMaster, nil case DurabilityLevelPersistToMajority: return memd.DurabilityLevelPersistToMajority, nil case DurabilityLevelUnknown: return 0, makeInvalidArgumentsError("unexpected unset durability level") default: return 0, makeInvalidArgumentsError("unexpected durability level") } } func durabilityLevelFromManagementAPI(level string) DurabilityLevel { switch level { case "majority": return DurabilityLevelMajority case "majorityAndPersistActive": return DurabilityLevelMajorityAndPersistOnMaster case "persistToMajority": return DurabilityLevelPersistToMajority default: return DurabilityLevelNone } } // MutationMacro can be supplied to MutateIn operations to perform ExpandMacros operations. type MutationMacro string const ( // MutationMacroCAS can be used to tell the server to use the CAS macro. MutationMacroCAS MutationMacro = "\"${Mutation.CAS}\"" // MutationMacroSeqNo can be used to tell the server to use the seqno macro. MutationMacroSeqNo MutationMacro = "\"${Mutation.seqno}\"" // MutationMacroValueCRC32c can be used to tell the server to use the value_crc32c macro. MutationMacroValueCRC32c MutationMacro = "\"${Mutation.value_crc32c}\"" ) // ClusterState specifies the current state of the cluster type ClusterState uint const ( // ClusterStateOnline indicates that all nodes are online and reachable. ClusterStateOnline ClusterState = iota + 1 // ClusterStateDegraded indicates that all services will function, but possibly not optimally. ClusterStateDegraded // ClusterStateOffline indicates that no nodes were reachable. ClusterStateOffline ) // EndpointState specifies the current state of an endpoint. type EndpointState uint const ( // EndpointStateDisconnected indicates the endpoint socket is unreachable. EndpointStateDisconnected EndpointState = iota + 1 // EndpointStateConnecting indicates the endpoint socket is connecting. EndpointStateConnecting // EndpointStateConnected indicates the endpoint socket is connected and ready. EndpointStateConnected // EndpointStateDisconnecting indicates the endpoint socket is disconnecting. EndpointStateDisconnecting ) // PingState specifies the result of the ping operation type PingState uint const ( // PingStateOk indicates that the ping operation was successful. PingStateOk PingState = iota + 1 // PingStateTimeout indicates that the ping operation timed out. PingStateTimeout // PingStateError indicates that the ping operation failed. PingStateError ) // SaslMechanism represents a type of auth that can be performed. type SaslMechanism string const ( // PlainSaslMechanism represents that PLAIN auth should be performed. PlainSaslMechanism SaslMechanism = SaslMechanism(gocbcore.PlainAuthMechanism) // ScramSha1SaslMechanism represents that SCRAM SHA1 auth should be performed. ScramSha1SaslMechanism SaslMechanism = SaslMechanism(gocbcore.ScramSha1AuthMechanism) // ScramSha256SaslMechanism represents that SCRAM SHA256 auth should be performed. ScramSha256SaslMechanism SaslMechanism = SaslMechanism(gocbcore.ScramSha256AuthMechanism) // ScramSha512SaslMechanism represents that SCRAM SHA512 auth should be performed. ScramSha512SaslMechanism SaslMechanism = SaslMechanism(gocbcore.ScramSha512AuthMechanism) ) // Capability represents a server capability. // Internal: This should never be used and is not supported. type Capability uint32 const ( CapabilityDurableWrites Capability = iota + 1 CapabilityCreateAsDeleted CapabilityReplaceBodyWithXattr ) // CapabilityStatus represents a status for a server capability. // Internal: This should never be used and is not supported. type CapabilityStatus uint32 const ( CapabilityStatusUnknown CapabilityStatus = CapabilityStatus(gocbcore.BucketCapabilityStatusUnknown) CapabilityStatusSupported CapabilityStatus = CapabilityStatus(gocbcore.BucketCapabilityStatusSupported) CapabilityStatusUnsupported CapabilityStatus = CapabilityStatus(gocbcore.BucketCapabilityStatusUnsupported) ) const ( spanNameDispatchToServer = "dispatch_to_server" spanNameRequestEncoding = "request_encoding" spanAttribDBSystemKey = "db.system" spanAttribDBSystemValue = "couchbase" spanAttribOperationIDKey = "db.couchbase.operation_id" spanAttribOperationKey = "db.operation" spanAttribLocalIDKey = "db.couchbase.local_id" spanAttribNetHostNameKey = "net.host.name" spanAttribNetHostPortKey = "net.host.port" spanAttribNetPeerNameKey = "net.peer.name" spanAttribNetPeerPortKey = "net.peer.port" spanAttribServerDurationKey = "db.couchbase.server_duration" spanAttribServiceKey = "db.couchbase.service" spanAttribDBNameKey = "db.name" spanAttribDBCollectionNameKey = "db.couchbase.collection" spanAttribDBScopeNameKey = "db.couchbase.scope" spanAttribDBDurability = "db.couchbase.durability" meterNameCBOperations = "db.couchbase.operations" meterAttribServiceKey = "db.couchbase.service" meterAttribOperationKey = "db.operation" meterValueServiceKV = "kv" meterValueServiceQuery = "query" meterValueServiceAnalytics = "analytics" meterValueServiceSearch = "search" meterValueServiceViews = "views" meterValueServiceManagement = "management" ) type AnalyticsLinkType string const ( AnalyticsLinkTypeS3External AnalyticsLinkType = "s3" AnalyticsLinkTypeAzureExternal AnalyticsLinkType = "azureblob" AnalyticsLinkTypeCouchbaseRemote AnalyticsLinkType = "couchbase" ) type AnalyticsEncryptionLevel uint8 const ( AnalyticsEncryptionLevelNone AnalyticsEncryptionLevel = iota AnalyticsEncryptionLevelHalf AnalyticsEncryptionLevelFull ) func (ael AnalyticsEncryptionLevel) String() string { switch ael { case AnalyticsEncryptionLevelNone: return "none" case AnalyticsEncryptionLevelHalf: return "half" case AnalyticsEncryptionLevelFull: return "full" } return "" } func analyticsEncryptionLevelFromString(level string) AnalyticsEncryptionLevel { switch level { case "none": return AnalyticsEncryptionLevelNone case "half": return AnalyticsEncryptionLevelHalf case "full": return AnalyticsEncryptionLevelFull } return AnalyticsEncryptionLevelNone } gocb-2.6.3/constants_str.go000066400000000000000000000021101441755043100156650ustar00rootroot00000000000000package gocb func serviceTypeToString(service ServiceType) string { switch service { case ServiceTypeManagement: return "mgmt" case ServiceTypeKeyValue: return "kv" case ServiceTypeViews: return "views" case ServiceTypeQuery: return "query" case ServiceTypeSearch: return "search" case ServiceTypeAnalytics: return "analytics" } return "" } func clusterStateToString(state ClusterState) string { switch state { case ClusterStateOnline: return "online" case ClusterStateDegraded: return "degraded" case ClusterStateOffline: return "offline" } return "" } func endpointStateToString(state EndpointState) string { switch state { case EndpointStateDisconnected: return "disconnected" case EndpointStateConnecting: return "connecting" case EndpointStateConnected: return "connected" case EndpointStateDisconnecting: return "disconnecting" } return "" } func pingStateToString(state PingState) string { switch state { case PingStateOk: return "ok" case PingStateTimeout: return "timeout" case PingStateError: return "error" } return "" } gocb-2.6.3/error.go000066400000000000000000000314551441755043100141300ustar00rootroot00000000000000package gocb import ( "errors" "fmt" gocbcore "github.com/couchbase/gocbcore/v10" ) type wrappedError struct { Message string InnerError error } func (e wrappedError) Error() string { return fmt.Sprintf("%s: %s", e.Message, e.InnerError.Error()) } func (e wrappedError) Unwrap() error { return e.InnerError } func wrapError(err error, message string) error { return wrappedError{ Message: message, InnerError: err, } } type invalidArgumentsError struct { message string } func (e invalidArgumentsError) Error() string { return fmt.Sprintf("invalid arguments: %s", e.message) } func (e invalidArgumentsError) Unwrap() error { return ErrInvalidArgument } func makeInvalidArgumentsError(message string) error { return invalidArgumentsError{ message: message, } } // Shared Error Definitions RFC#58@15 var ( // ErrTimeout occurs when an operation does not receive a response in a timely manner. ErrTimeout = gocbcore.ErrTimeout // ErrRequestCanceled occurs when an operation has been canceled. ErrRequestCanceled = gocbcore.ErrRequestCanceled // ErrInvalidArgument occurs when an invalid argument is provided for an operation. ErrInvalidArgument = gocbcore.ErrInvalidArgument // ErrServiceNotAvailable occurs when the requested service is not available. ErrServiceNotAvailable = gocbcore.ErrServiceNotAvailable // ErrInternalServerFailure occurs when the server encounters an internal server error. ErrInternalServerFailure = gocbcore.ErrInternalServerFailure // ErrAuthenticationFailure occurs when authentication has failed. ErrAuthenticationFailure = gocbcore.ErrAuthenticationFailure // ErrTemporaryFailure occurs when an operation has failed for a reason that is temporary. ErrTemporaryFailure = gocbcore.ErrTemporaryFailure // ErrParsingFailure occurs when a query has failed to be parsed by the server. ErrParsingFailure = gocbcore.ErrParsingFailure // ErrCasMismatch occurs when an operation has been performed with a cas value that does not the value on the server. ErrCasMismatch = gocbcore.ErrCasMismatch // ErrBucketNotFound occurs when the requested bucket could not be found. ErrBucketNotFound = gocbcore.ErrBucketNotFound // ErrCollectionNotFound occurs when the requested collection could not be found. ErrCollectionNotFound = gocbcore.ErrCollectionNotFound // ErrEncodingFailure occurs when encoding of a value failed. ErrEncodingFailure = gocbcore.ErrEncodingFailure // ErrDecodingFailure occurs when decoding of a value failed. ErrDecodingFailure = gocbcore.ErrDecodingFailure // ErrUnsupportedOperation occurs when an operation that is unsupported or unknown is performed against the server. ErrUnsupportedOperation = gocbcore.ErrUnsupportedOperation // ErrAmbiguousTimeout occurs when an operation does not receive a response in a timely manner for a reason that // ErrAmbiguousTimeout = gocbcore.ErrAmbiguousTimeout // ErrAmbiguousTimeout occurs when an operation does not receive a response in a timely manner for a reason that // it can be safely established that ErrUnambiguousTimeout = gocbcore.ErrUnambiguousTimeout // ErrFeatureNotAvailable occurs when an operation is performed on a bucket which does not support it. ErrFeatureNotAvailable = gocbcore.ErrFeatureNotAvailable // ErrScopeNotFound occurs when the requested scope could not be found. ErrScopeNotFound = gocbcore.ErrScopeNotFound // ErrIndexNotFound occurs when the requested index could not be found. ErrIndexNotFound = gocbcore.ErrIndexNotFound // ErrIndexExists occurs when creating an index that already exists. ErrIndexExists = gocbcore.ErrIndexExists // ErrRateLimitedFailure occurs when a request is rate limited by the server. ErrRateLimitedFailure = gocbcore.ErrRateLimitedFailure // ErrQuotaLimitedFailure occurs when a request triggers a resource to exceed the allowed quota. ErrQuotaLimitedFailure = gocbcore.ErrQuotaLimitedFailure ) // Key Value Error Definitions RFC#58@15 var ( // ErrDocumentNotFound occurs when the requested document could not be found. ErrDocumentNotFound = gocbcore.ErrDocumentNotFound // ErrDocumentUnretrievable occurs when GetAnyReplica cannot find the document on any replica. ErrDocumentUnretrievable = gocbcore.ErrDocumentUnretrievable // ErrDocumentLocked occurs when a mutation operation is attempted against a document that is locked. ErrDocumentLocked = gocbcore.ErrDocumentLocked // ErrValueTooLarge occurs when a document has gone over the maximum size allowed by the server. ErrValueTooLarge = gocbcore.ErrValueTooLarge // ErrDocumentExists occurs when an attempt is made to insert a document but a document with that key already exists. ErrDocumentExists = gocbcore.ErrDocumentExists // ErrValueNotJSON occurs when a sub-document operation is performed on a // document which is not JSON. ErrValueNotJSON = gocbcore.ErrValueNotJSON // ErrDurabilityLevelNotAvailable occurs when an invalid durability level was requested. ErrDurabilityLevelNotAvailable = gocbcore.ErrDurabilityLevelNotAvailable // ErrDurabilityImpossible occurs when a request is performed with impossible // durability level requirements. ErrDurabilityImpossible = gocbcore.ErrDurabilityImpossible // ErrDurabilityAmbiguous occurs when an SyncWrite does not complete in the specified // time and the result is ambiguous. ErrDurabilityAmbiguous = gocbcore.ErrDurabilityAmbiguous // ErrDurableWriteInProgress occurs when an attempt is made to write to a key that has // a SyncWrite pending. ErrDurableWriteInProgress = gocbcore.ErrDurableWriteInProgress // ErrDurableWriteReCommitInProgress occurs when an SyncWrite is being recommitted. ErrDurableWriteReCommitInProgress = gocbcore.ErrDurableWriteReCommitInProgress // ErrMutationLost occurs when a mutation was lost. ErrMutationLost = gocbcore.ErrMutationLost // ErrPathNotFound occurs when a sub-document operation targets a path // which does not exist in the specified document. ErrPathNotFound = gocbcore.ErrPathNotFound // ErrPathMismatch occurs when a sub-document operation specifies a path // which does not match the document structure (field access on an array). ErrPathMismatch = gocbcore.ErrPathMismatch // ErrPathInvalid occurs when a sub-document path could not be parsed. ErrPathInvalid = gocbcore.ErrPathInvalid // ErrPathTooBig occurs when a sub-document path is too big. ErrPathTooBig = gocbcore.ErrPathTooBig // ErrPathTooDeep occurs when an operation would cause a document to be // nested beyond the depth limits allowed by the sub-document specification. ErrPathTooDeep = gocbcore.ErrPathTooDeep // ErrValueTooDeep occurs when a sub-document operation specifies a value // which is deeper than the depth limits of the sub-document specification. ErrValueTooDeep = gocbcore.ErrValueTooDeep // ErrValueInvalid occurs when a sub-document operation could not insert. ErrValueInvalid = gocbcore.ErrValueInvalid // ErrDocumentNotJSON occurs when a sub-document operation is performed on a // document which is not JSON. ErrDocumentNotJSON = gocbcore.ErrDocumentNotJSON // ErrNumberTooBig occurs when a sub-document operation is performed with // a bad range. ErrNumberTooBig = gocbcore.ErrNumberTooBig // ErrDeltaInvalid occurs when a sub-document counter operation is performed // and the specified delta is not valid. ErrDeltaInvalid = gocbcore.ErrDeltaInvalid // ErrPathExists occurs when a sub-document operation expects a path not // to exists, but the path was found in the document. ErrPathExists = gocbcore.ErrPathExists // ErrXattrUnknownMacro occurs when an invalid macro value is specified. ErrXattrUnknownMacro = gocbcore.ErrXattrUnknownMacro // ErrXattrInvalidFlagCombo occurs when an invalid set of // extended-attribute flags is passed to a sub-document operation. ErrXattrInvalidFlagCombo = gocbcore.ErrXattrInvalidFlagCombo // ErrXattrInvalidKeyCombo occurs when an invalid set of key operations // are specified for a extended-attribute sub-document operation. ErrXattrInvalidKeyCombo = gocbcore.ErrXattrInvalidKeyCombo // ErrXattrUnknownVirtualAttribute occurs when an invalid virtual attribute is specified. ErrXattrUnknownVirtualAttribute = gocbcore.ErrXattrUnknownVirtualAttribute // ErrXattrCannotModifyVirtualAttribute occurs when a mutation is attempted upon // a virtual attribute (which are immutable by definition). ErrXattrCannotModifyVirtualAttribute = gocbcore.ErrXattrCannotModifyVirtualAttribute // ErrXattrInvalidOrder occurs when a set key key operations are specified for a extended-attribute sub-document // operation in the incorrect order. ErrXattrInvalidOrder = gocbcore.ErrXattrInvalidOrder ) // Query Error Definitions RFC#58@15 var ( // ErrPlanningFailure occurs when the query service was unable to create a query plan. ErrPlanningFailure = gocbcore.ErrPlanningFailure // ErrIndexFailure occurs when there was an issue with the index specified. ErrIndexFailure = gocbcore.ErrIndexFailure // ErrPreparedStatementFailure occurs when there was an issue with the prepared statement. ErrPreparedStatementFailure = gocbcore.ErrPreparedStatementFailure ) // Analytics Error Definitions RFC#58@15 var ( // ErrCompilationFailure occurs when there was an issue executing the analytics query because it could not // be compiled. ErrCompilationFailure = gocbcore.ErrCompilationFailure // ErrJobQueueFull occurs when the analytics service job queue is full. ErrJobQueueFull = gocbcore.ErrJobQueueFull // ErrDatasetNotFound occurs when the analytics dataset requested could not be found. ErrDatasetNotFound = gocbcore.ErrDatasetNotFound // ErrDataverseNotFound occurs when the analytics dataverse requested could not be found. ErrDataverseNotFound = gocbcore.ErrDataverseNotFound // ErrDatasetExists occurs when creating an analytics dataset failed because it already exists. ErrDatasetExists = gocbcore.ErrDatasetExists // ErrDataverseExists occurs when creating an analytics dataverse failed because it already exists. ErrDataverseExists = gocbcore.ErrDataverseExists // ErrLinkNotFound occurs when the analytics link requested could not be found. ErrLinkNotFound = gocbcore.ErrLinkNotFound // ErrAnalyticsLinkExists occurs when the analytics link already exists. ErrAnalyticsLinkExists = errors.New("analytics clink already exists") ) // Search Error Definitions RFC#58@15 var () // View Error Definitions RFC#58@15 var ( // ErrViewNotFound occurs when the view requested could not be found. ErrViewNotFound = gocbcore.ErrViewNotFound // ErrDesignDocumentNotFound occurs when the design document requested could not be found. ErrDesignDocumentNotFound = gocbcore.ErrDesignDocumentNotFound ) // Management Error Definitions RFC#58@15 var ( // ErrCollectionExists occurs when creating a collection failed because it already exists. ErrCollectionExists = gocbcore.ErrCollectionExists // ErrScopeExists occurs when creating a scope failed because it already exists. ErrScopeExists = gocbcore.ErrScopeExists // ErrUserNotFound occurs when the user requested could not be found. ErrUserNotFound = gocbcore.ErrUserNotFound // ErrGroupNotFound occurs when the group requested could not be found. ErrGroupNotFound = gocbcore.ErrGroupNotFound // ErrBucketExists occurs when creating a bucket failed because it already exists. ErrBucketExists = gocbcore.ErrBucketExists // ErrUserExists occurs when creating a user failed because it already exists. ErrUserExists = gocbcore.ErrUserExists // ErrBucketNotFlushable occurs when a bucket could not be flushed because flushing is not enabled. ErrBucketNotFlushable = gocbcore.ErrBucketNotFlushable // ErrEventingFunctionNotFound occurs when the eventing function requested could not be found. ErrEventingFunctionNotFound = gocbcore.ErrEventingFunctionNotFound // ErrEventingFunctionNotDeployed occurs when the eventing function requested is not deployed. ErrEventingFunctionNotDeployed = gocbcore.ErrEventingFunctionNotDeployed // ErrEventingFunctionCompilationFailure occurs when the eventing function requested could not be compiled. ErrEventingFunctionCompilationFailure = gocbcore.ErrEventingFunctionCompilationFailure // ErrEventingFunctionIdenticalKeyspace occurs when the eventing function requested uses the same keyspace for source and metadata. ErrEventingFunctionIdenticalKeyspace = gocbcore.ErrEventingFunctionIdenticalKeyspace // ErrEventingFunctionNotBootstrapped occurs when the eventing function requested is not bootstrapped. ErrEventingFunctionNotBootstrapped = gocbcore.ErrEventingFunctionNotBootstrapped // ErrEventingFunctionDeployed occurs when the eventing function requested is not undeployed. ErrEventingFunctionDeployed = gocbcore.ErrEventingFunctionNotUndeployed ) // SDK specific error definitions var ( // ErrOverload occurs when too many operations are dispatched and all queues are full. ErrOverload = gocbcore.ErrOverload // ErrNoResult occurs when no results are available to a query. ErrNoResult = errors.New("no result was available") ) gocb-2.6.3/error_analytics.go000066400000000000000000000071721441755043100161760ustar00rootroot00000000000000package gocb import ( "encoding/json" gocbcore "github.com/couchbase/gocbcore/v10" ) // AnalyticsErrorDesc represents a specific error returned from the analytics service. type AnalyticsErrorDesc struct { Code uint32 Message string } func translateCoreAnalyticsErrorDesc(descs []gocbcore.AnalyticsErrorDesc) []AnalyticsErrorDesc { descsOut := make([]AnalyticsErrorDesc, len(descs)) for descIdx, desc := range descs { descsOut[descIdx] = AnalyticsErrorDesc{ Code: desc.Code, Message: desc.Message, } } return descsOut } // AnalyticsError is the error type of all analytics query errors. // UNCOMMITTED: This API may change in the future. type AnalyticsError struct { InnerError error `json:"-"` Statement string `json:"statement,omitempty"` ClientContextID string `json:"client_context_id,omitempty"` Errors []AnalyticsErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` } // MarshalJSON implements the Marshaler interface. func (e AnalyticsError) MarshalJSON() ([]byte, error) { var innerError string if e.InnerError != nil { innerError = e.InnerError.Error() } return json.Marshal(struct { InnerError string `json:"msg,omitempty"` Statement string `json:"statement,omitempty"` ClientContextID string `json:"client_context_id,omitempty"` Errors []AnalyticsErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: innerError, Statement: e.Statement, ClientContextID: e.ClientContextID, Errors: e.Errors, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, HTTPStatusCode: e.HTTPStatusCode, }) } // Error returns the string representation of this error. func (e AnalyticsError) Error() string { errBytes, serErr := json.Marshal(struct { InnerError error `json:"-"` Statement string `json:"statement,omitempty"` ClientContextID string `json:"client_context_id,omitempty"` Errors []AnalyticsErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: e.InnerError, Statement: e.Statement, ClientContextID: e.ClientContextID, Errors: e.Errors, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, ErrorText: e.ErrorText, HTTPStatusCode: e.HTTPStatusCode, }) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return e.InnerError.Error() + " | " + string(errBytes) } // Unwrap returns the underlying cause for this error. func (e AnalyticsError) Unwrap() error { return e.InnerError } gocb-2.6.3/error_analytics_test.go000066400000000000000000000021551441755043100172310ustar00rootroot00000000000000package gocb import ( "encoding/json" ) func (suite *UnitTestSuite) TestAnalyticsError() { aErr := AnalyticsError{ InnerError: ErrDatasetNotFound, Statement: "select * from dataset", ClientContextID: "12345", Errors: []AnalyticsErrorDesc{{ Code: 1000, Message: "error 1000", }}, Endpoint: "http://127.0.0.1:8095", RetryReasons: []RetryReason{AnalyticsTemporaryFailureRetryReason}, RetryAttempts: 3, } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( []byte("{\"msg\":\"dataset not found\",\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"Code\":1000,\"Message\":\"error 1000\"}],\"endpoint\":\"http://127.0.0.1:8095\",\"retry_reasons\":[\"ANALYTICS_TEMPORARY_FAILURE\"],\"retry_attempts\":3}"), b, ) suite.Assert().Equal( "dataset not found | {\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"Code\":1000,\"Message\":\"error 1000\"}],\"endpoint\":\"http://127.0.0.1:8095\",\"retry_reasons\":[\"ANALYTICS_TEMPORARY_FAILURE\"],\"retry_attempts\":3}", aErr.Error(), ) } gocb-2.6.3/error_http.go000066400000000000000000000072661441755043100151720ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "github.com/couchbase/gocbcore/v10" "io/ioutil" ) // HTTPError is the error type of management HTTP errors. // UNCOMMITTED: This API may change in the future. type HTTPError struct { InnerError error `json:"-"` UniqueID string `json:"unique_id,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` StatusCode uint32 `json:"status_code,omitempty"` } // MarshalJSON implements the Marshaler interface. func (e HTTPError) MarshalJSON() ([]byte, error) { var innerError string if e.InnerError != nil { innerError = e.InnerError.Error() } return json.Marshal(struct { InnerError string `json:"msg,omitempty"` UniqueID string `json:"unique_id,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` StatusCode uint32 `json:"status_code,omitempty"` }{ InnerError: innerError, UniqueID: e.UniqueID, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, ErrorText: e.ErrorText, StatusCode: e.StatusCode, }) } // Error returns the string representation of this error. func (e HTTPError) Error() string { errBytes, serErr := json.Marshal(struct { InnerError error `json:"-"` UniqueID string `json:"unique_id,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` StatusCode uint32 `json:"status_code,omitempty"` }{ InnerError: e.InnerError, UniqueID: e.UniqueID, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, ErrorText: e.ErrorText, StatusCode: e.StatusCode, }) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return e.InnerError.Error() + " | " + string(errBytes) } // Unwrap returns the underlying cause for this error. func (e HTTPError) Unwrap() error { return e.InnerError } func makeGenericHTTPError(baseErr error, req *gocbcore.HTTPRequest, resp *gocbcore.HTTPResponse) error { if baseErr == nil { logErrorf("makeGenericHTTPError got an empty error") baseErr = errors.New("unknown error") } err := HTTPError{ InnerError: baseErr, } if req != nil { err.UniqueID = req.UniqueID } if resp != nil { err.Endpoint = resp.Endpoint err.StatusCode = uint32(resp.StatusCode) } return err } func makeGenericMgmtError(baseErr error, req *mgmtRequest, resp *mgmtResponse, errText string) error { if baseErr == nil { logErrorf("makeGenericMgmtError got an empty error") baseErr = errors.New("unknown error") } err := HTTPError{ InnerError: baseErr, ErrorText: errText, } if req != nil { err.UniqueID = req.UniqueID } if resp != nil { err.Endpoint = resp.Endpoint err.StatusCode = resp.StatusCode } return err } func makeMgmtBadStatusError(message string, req *mgmtRequest, resp *mgmtResponse) error { var errText string if resp != nil { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("failed to read http body: %s", err) return nil } errText = string(b) } return makeGenericMgmtError(errors.New(message), req, resp, errText) } gocb-2.6.3/error_http_test.go000066400000000000000000000011041441755043100162120ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" ) func (suite *UnitTestSuite) TestHTTPError() { aErr := HTTPError{ InnerError: errors.New("uh oh"), Endpoint: "http://127.0.0.1:8091", UniqueID: "123445", RetryReasons: nil, RetryAttempts: 0, } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( []byte("{\"msg\":\"uh oh\",\"unique_id\":\"123445\",\"endpoint\":\"http://127.0.0.1:8091\"}"), b, ) suite.Assert().Equal( "uh oh | {\"unique_id\":\"123445\",\"endpoint\":\"http://127.0.0.1:8091\"}", aErr.Error(), ) } gocb-2.6.3/error_keyvalue.go000066400000000000000000000127041441755043100160310ustar00rootroot00000000000000package gocb import ( "encoding/json" "github.com/couchbase/gocbcore/v10/memd" ) // KeyValueError wraps key-value errors that occur within the SDK. // UNCOMMITTED: This API may change in the future. type KeyValueError struct { InnerError error `json:"-"` StatusCode memd.StatusCode `json:"status_code,omitempty"` DocumentID string `json:"document_id,omitempty"` BucketName string `json:"bucket,omitempty"` ScopeName string `json:"scope,omitempty"` CollectionName string `json:"collection,omitempty"` CollectionID uint32 `json:"collection_id,omitempty"` ErrorName string `json:"error_name,omitempty"` ErrorDescription string `json:"error_description,omitempty"` Opaque uint32 `json:"opaque,omitempty"` Context string `json:"context,omitempty"` Ref string `json:"ref,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` LastDispatchedTo string `json:"last_dispatched_to,omitempty"` LastDispatchedFrom string `json:"last_dispatched_from,omitempty"` LastConnectionID string `json:"last_connection_id,omitempty"` } // MarshalJSON implements the Marshaler interface. func (e KeyValueError) MarshalJSON() ([]byte, error) { var innerError string if e.InnerError != nil { innerError = e.InnerError.Error() } return json.Marshal(struct { InnerError string `json:"msg,omitempty"` StatusCode memd.StatusCode `json:"status_code,omitempty"` DocumentID string `json:"document_id,omitempty"` BucketName string `json:"bucket,omitempty"` ScopeName string `json:"scope,omitempty"` CollectionName string `json:"collection,omitempty"` CollectionID uint32 `json:"collection_id,omitempty"` ErrorName string `json:"error_name,omitempty"` ErrorDescription string `json:"error_description,omitempty"` Opaque uint32 `json:"opaque,omitempty"` Context string `json:"context,omitempty"` Ref string `json:"ref,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` LastDispatchedTo string `json:"last_dispatched_to,omitempty"` LastDispatchedFrom string `json:"last_dispatched_from,omitempty"` LastConnectionID string `json:"last_connection_id,omitempty"` }{ InnerError: innerError, StatusCode: e.StatusCode, DocumentID: e.DocumentID, BucketName: e.BucketName, ScopeName: e.ScopeName, CollectionName: e.CollectionName, CollectionID: e.CollectionID, ErrorName: e.ErrorName, ErrorDescription: e.ErrorDescription, Opaque: e.Opaque, Context: e.Context, Ref: e.Ref, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, LastDispatchedTo: e.LastDispatchedTo, LastDispatchedFrom: e.LastDispatchedFrom, LastConnectionID: e.LastConnectionID, }) } // Error returns the string representation of a kv error. func (e KeyValueError) Error() string { errBytes, serErr := json.Marshal(struct { InnerError error `json:"-"` StatusCode memd.StatusCode `json:"status_code,omitempty"` DocumentID string `json:"document_id,omitempty"` BucketName string `json:"bucket,omitempty"` ScopeName string `json:"scope,omitempty"` CollectionName string `json:"collection,omitempty"` CollectionID uint32 `json:"collection_id,omitempty"` ErrorName string `json:"error_name,omitempty"` ErrorDescription string `json:"error_description,omitempty"` Opaque uint32 `json:"opaque,omitempty"` Context string `json:"context,omitempty"` Ref string `json:"ref,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` LastDispatchedTo string `json:"last_dispatched_to,omitempty"` LastDispatchedFrom string `json:"last_dispatched_from,omitempty"` LastConnectionID string `json:"last_connection_id,omitempty"` }{ InnerError: e.InnerError, StatusCode: e.StatusCode, DocumentID: e.DocumentID, BucketName: e.BucketName, ScopeName: e.ScopeName, CollectionName: e.CollectionName, CollectionID: e.CollectionID, ErrorName: e.ErrorName, ErrorDescription: e.ErrorDescription, Opaque: e.Opaque, Context: e.Context, Ref: e.Ref, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, LastDispatchedTo: e.LastDispatchedTo, LastDispatchedFrom: e.LastDispatchedFrom, LastConnectionID: e.LastConnectionID, }) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return e.InnerError.Error() + " | " + string(errBytes) } // Unwrap returns the underlying reason for the error func (e KeyValueError) Unwrap() error { return e.InnerError } gocb-2.6.3/error_keyvalue_test.go000066400000000000000000000032051441755043100170640ustar00rootroot00000000000000package gocb import ( "encoding/json" "github.com/couchbase/gocbcore/v10/memd" ) func (suite *UnitTestSuite) TestKeyValueError() { aErr := KeyValueError{ InnerError: ErrPathNotFound, StatusCode: memd.StatusBusy, DocumentID: "key", BucketName: "bucket", ScopeName: "scope", CollectionName: "collection", CollectionID: 9, ErrorName: "barry", ErrorDescription: "sheen", Opaque: 0xa1, RetryReasons: []RetryReason{CircuitBreakerOpenRetryReason}, RetryAttempts: 3, LastDispatchedTo: "10.112.210.101", LastDispatchedFrom: "10.112.210.1", LastConnectionID: "123456", } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( []byte("{\"msg\":\"path not found\",\"status_code\":133,\"document_id\":\"key\",\"bucket\":\"bucket\",\"scope\":\"scope\",\"collection\":\"collection\",\"collection_id\":9,\"error_name\":\"barry\",\"error_description\":\"sheen\",\"opaque\":161,\"retry_reasons\":[\"CIRCUIT_BREAKER_OPEN\"],\"retry_attempts\":3,\"last_dispatched_to\":\"10.112.210.101\",\"last_dispatched_from\":\"10.112.210.1\",\"last_connection_id\":\"123456\"}"), b, ) suite.Assert().Equal( "path not found | {\"status_code\":133,\"document_id\":\"key\",\"bucket\":\"bucket\",\"scope\":\"scope\",\"collection\":\"collection\",\"collection_id\":9,\"error_name\":\"barry\",\"error_description\":\"sheen\",\"opaque\":161,\"retry_reasons\":[\"CIRCUIT_BREAKER_OPEN\"],\"retry_attempts\":3,\"last_dispatched_to\":\"10.112.210.101\",\"last_dispatched_from\":\"10.112.210.1\",\"last_connection_id\":\"123456\"}", aErr.Error(), ) } gocb-2.6.3/error_query.go000066400000000000000000000077561441755043100153640ustar00rootroot00000000000000package gocb import ( "encoding/json" gocbcore "github.com/couchbase/gocbcore/v10" ) // QueryErrorDesc represents a specific error returned from the query service. type QueryErrorDesc struct { Code uint32 Message string Retry bool Reason map[string]interface{} } // MarshalJSON implements the Marshaler interface. func (e QueryErrorDesc) MarshalJSON() ([]byte, error) { return json.Marshal(struct { Code uint32 `json:"code"` Message string `json:"message"` Retry bool `json:"retry,omitempty"` Reason map[string]interface{} `json:"reason,omitempty"` }{ Code: e.Code, Message: e.Message, Retry: e.Retry, Reason: e.Reason, }) } func translateCoreQueryErrorDesc(descs []gocbcore.N1QLErrorDesc) []QueryErrorDesc { descsOut := make([]QueryErrorDesc, len(descs)) for descIdx, desc := range descs { descsOut[descIdx] = QueryErrorDesc{ Code: desc.Code, Message: desc.Message, Retry: desc.Retry, Reason: desc.Reason, } } return descsOut } // QueryError is the error type of all query errors. // UNCOMMITTED: This API may change in the future. type QueryError struct { InnerError error `json:"-"` Statement string `json:"statement,omitempty"` ClientContextID string `json:"client_context_id,omitempty"` Errors []QueryErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` } // MarshalJSON implements the Marshaler interface. func (e QueryError) MarshalJSON() ([]byte, error) { var innerError string if e.InnerError != nil { innerError = e.InnerError.Error() } return json.Marshal(struct { InnerError string `json:"msg,omitempty"` Statement string `json:"statement,omitempty"` ClientContextID string `json:"client_context_id,omitempty"` Errors []QueryErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: innerError, Statement: e.Statement, ClientContextID: e.ClientContextID, Errors: e.Errors, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, HTTPStatusCode: e.HTTPStatusCode, }) } // Error returns the string representation of this error. func (e QueryError) Error() string { errBytes, serErr := json.Marshal(struct { InnerError error `json:"-"` Statement string `json:"statement,omitempty"` ClientContextID string `json:"client_context_id,omitempty"` Errors []QueryErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: e.InnerError, Statement: e.Statement, ClientContextID: e.ClientContextID, Errors: e.Errors, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, ErrorText: e.ErrorText, HTTPStatusCode: e.HTTPStatusCode, }) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return e.InnerError.Error() + " | " + string(errBytes) } // Unwrap returns the underlying cause for this error. func (e QueryError) Unwrap() error { return e.InnerError } gocb-2.6.3/error_query_test.go000066400000000000000000000044011441755043100164030ustar00rootroot00000000000000package gocb import "encoding/json" func (suite *UnitTestSuite) TestQueryError() { aErr := QueryError{ InnerError: ErrIndexFailure, Statement: "select * from dataset", ClientContextID: "12345", Errors: []QueryErrorDesc{{ Code: 1000, Message: "error 1000", }}, Endpoint: "http://127.0.0.1:8093", RetryReasons: []RetryReason{QueryIndexNotFoundRetryReason}, RetryAttempts: 3, } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( "{\"msg\":\"index failure\",\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"code\":1000,\"message\":\"error 1000\"}],\"endpoint\":\"http://127.0.0.1:8093\",\"retry_reasons\":[\"QUERY_INDEX_NOT_FOUND\"],\"retry_attempts\":3}", string(b), ) suite.Assert().Equal( "index failure | {\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"code\":1000,\"message\":\"error 1000\"}],\"endpoint\":\"http://127.0.0.1:8093\",\"retry_reasons\":[\"QUERY_INDEX_NOT_FOUND\"],\"retry_attempts\":3}", aErr.Error(), ) } func (suite *UnitTestSuite) TestQueryErrorImproved() { aErr := QueryError{ InnerError: ErrIndexFailure, Statement: "select * from dataset", ClientContextID: "12345", Errors: []QueryErrorDesc{{ Code: 1000, Message: "error 1000", Reason: map[string]interface{}{ "code": 17029, }, Retry: true, }}, Endpoint: "http://127.0.0.1:8093", RetryReasons: []RetryReason{QueryIndexNotFoundRetryReason}, RetryAttempts: 3, } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( "{\"msg\":\"index failure\",\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"code\":1000,\"message\":\"error 1000\",\"retry\":true,\"reason\":{\"code\":17029}}],\"endpoint\":\"http://127.0.0.1:8093\",\"retry_reasons\":[\"QUERY_INDEX_NOT_FOUND\"],\"retry_attempts\":3}", string(b), ) suite.Assert().Equal( "index failure | {\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"code\":1000,\"message\":\"error 1000\",\"retry\":true,\"reason\":{\"code\":17029}}],\"endpoint\":\"http://127.0.0.1:8093\",\"retry_reasons\":[\"QUERY_INDEX_NOT_FOUND\"],\"retry_attempts\":3}", aErr.Error(), ) } gocb-2.6.3/error_search.go000066400000000000000000000054061441755043100154520ustar00rootroot00000000000000package gocb import ( "encoding/json" ) // SearchError is the error type of all search query errors. // UNCOMMITTED: This API may change in the future. type SearchError struct { InnerError error `json:"-"` Query interface{} `json:"query,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text"` IndexName string `json:"index_name,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` } // MarshalJSON implements the Marshaler interface. func (e SearchError) MarshalJSON() ([]byte, error) { var innerError string if e.InnerError != nil { innerError = e.InnerError.Error() } return json.Marshal(struct { InnerError string `json:"msg,omitempty"` IndexName string `json:"index_name,omitempty"` Query interface{} `json:"query,omitempty"` ErrorText string `json:"error_text"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: innerError, IndexName: e.IndexName, Query: e.Query, ErrorText: e.ErrorText, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, HTTPStatusCode: e.HTTPStatusCode, }) } // Error returns the string representation of this error. func (e SearchError) Error() string { errBytes, serErr := json.Marshal(struct { InnerError error `json:"-"` IndexName string `json:"index_name,omitempty"` Query interface{} `json:"query,omitempty"` ErrorText string `json:"error_text"` HTTPResponseCode int `json:"status_code,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: e.InnerError, IndexName: e.IndexName, Query: e.Query, ErrorText: e.ErrorText, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, HTTPStatusCode: e.HTTPStatusCode, }) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return e.InnerError.Error() + " | " + string(errBytes) } // Unwrap returns the underlying cause for this error. func (e SearchError) Unwrap() error { return e.InnerError } gocb-2.6.3/error_search_test.go000066400000000000000000000017551441755043100165140ustar00rootroot00000000000000package gocb import ( "encoding/json" "github.com/couchbase/gocb/v2/search" ) func (suite *UnitTestSuite) TestSearchError() { aErr := SearchError{ InnerError: ErrIndexFailure, Query: search.NewMatchAllQuery(), Endpoint: "http://127.0.0.1:8094", RetryReasons: []RetryReason{SearchTooManyRequestsRetryReason}, RetryAttempts: 3, ErrorText: "error text", IndexName: "barry", } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( []byte("{\"msg\":\"index failure\",\"index_name\":\"barry\",\"query\":{\"match_all\":null},\"error_text\":\"error text\",\"endpoint\":\"http://127.0.0.1:8094\",\"retry_reasons\":[\"SEARCH_TOO_MANY_REQUESTS\"],\"retry_attempts\":3}"), b, ) suite.Assert().Equal( "index failure | {\"index_name\":\"barry\",\"query\":{\"match_all\":null},\"error_text\":\"error text\",\"endpoint\":\"http://127.0.0.1:8094\",\"retry_reasons\":[\"SEARCH_TOO_MANY_REQUESTS\"],\"retry_attempts\":3}", aErr.Error(), ) } gocb-2.6.3/error_timeout.go000066400000000000000000000047301441755043100156720ustar00rootroot00000000000000package gocb import ( "encoding/json" "time" ) // TimeoutError wraps timeout errors that occur within the SDK. // UNCOMMITTED: This API may change in the future. type TimeoutError struct { InnerError error OperationID string Opaque string TimeObserved time.Duration RetryReasons []RetryReason RetryAttempts uint32 LastDispatchedTo string LastDispatchedFrom string LastConnectionID string } type timeoutError struct { InnerError error `json:"-"` OperationID string `json:"s,omitempty"` Opaque string `json:"i,omitempty"` TimeObserved uint64 `json:"t,omitempty"` RetryReasons []string `json:"rr,omitempty"` RetryAttempts uint32 `json:"ra,omitempty"` LastDispatchedTo string `json:"r,omitempty"` LastDispatchedFrom string `json:"l,omitempty"` LastConnectionID string `json:"c,omitempty"` } // MarshalJSON implements the Marshaler interface. func (err *TimeoutError) MarshalJSON() ([]byte, error) { var retries []string for _, rr := range err.RetryReasons { retries = append(retries, rr.Description()) } toMarshal := timeoutError{ InnerError: err.InnerError, OperationID: err.OperationID, Opaque: err.Opaque, TimeObserved: uint64(err.TimeObserved / time.Microsecond), RetryReasons: retries, RetryAttempts: err.RetryAttempts, LastDispatchedTo: err.LastDispatchedTo, LastDispatchedFrom: err.LastDispatchedFrom, LastConnectionID: err.LastConnectionID, } return json.Marshal(toMarshal) } // UnmarshalJSON implements the Unmarshaler interface. func (err *TimeoutError) UnmarshalJSON(data []byte) error { var tErr *timeoutError if err := json.Unmarshal(data, &tErr); err != nil { return err } duration := time.Duration(tErr.TimeObserved) * time.Microsecond // Note that we cannot reasonably unmarshal the retry reasons err.OperationID = tErr.OperationID err.Opaque = tErr.Opaque err.TimeObserved = duration err.RetryAttempts = tErr.RetryAttempts err.LastDispatchedTo = tErr.LastDispatchedTo err.LastDispatchedFrom = tErr.LastDispatchedFrom err.LastConnectionID = tErr.LastConnectionID return nil } func (err TimeoutError) Error() string { if err.InnerError == nil { return serializeWrappedError(err) } return err.InnerError.Error() + " | " + serializeWrappedError(err) } // Unwrap returns the underlying reason for the error func (err TimeoutError) Unwrap() error { return err.InnerError } gocb-2.6.3/error_timeout_test.go000066400000000000000000000101431441755043100167240ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "time" "github.com/couchbase/gocbcore/v10" ) func (suite *UnitTestSuite) TestTimeoutError() { err := &gocbcore.TimeoutError{ InnerError: gocbcore.ErrAmbiguousTimeout, OperationID: "Get", Opaque: "0x09", TimeObserved: 100 * time.Millisecond, RetryReasons: []gocbcore.RetryReason{gocbcore.KVLockedRetryReason, gocbcore.CircuitBreakerOpenRetryReason}, RetryAttempts: 5, LastDispatchedTo: "127.0.0.1:56830", LastDispatchedFrom: "127.0.0.1:56839", LastConnectionID: "d323bee8e92a20d6/63ac5d0cc19a1334", } enhancedErr := maybeEnhanceKVErr(err, "", "", "", "bob") if !errors.Is(enhancedErr, ErrTimeout) { suite.T().Fatalf("Error should have been ErrTimeout but was %v", enhancedErr) } var tErr *TimeoutError if !errors.As(enhancedErr, &tErr) { suite.T().Fatalf("Error should have been TimeoutError but was %v", enhancedErr) } suite.Assert().Equal(tErr.InnerError, err.InnerError) suite.Assert().Equal(tErr.OperationID, err.OperationID) suite.Assert().Equal(tErr.Opaque, err.Opaque) suite.Assert().Equal(tErr.TimeObserved, err.TimeObserved) suite.Assert().Equal(tErr.RetryReasons, []RetryReason{KVLockedRetryReason, CircuitBreakerOpenRetryReason}) suite.Assert().Equal(tErr.RetryAttempts, err.RetryAttempts) suite.Assert().Equal(tErr.LastDispatchedTo, err.LastDispatchedTo) suite.Assert().Equal(tErr.LastDispatchedFrom, err.LastDispatchedFrom) suite.Assert().Equal(tErr.LastConnectionID, err.LastConnectionID) b, mErr := json.Marshal(tErr) suite.Require().Nil(mErr, mErr) expectedJSON := `{"s":"Get","i":"0x09","t":100000,"rr":["KV_LOCKED","CIRCUIT_BREAKER_OPEN"],"ra":5,"r":"127.0.0.1:56830","l":"127.0.0.1:56839","c":"d323bee8e92a20d6/63ac5d0cc19a1334"}` suite.Assert().Equal(expectedJSON, string(b)) var tErr2 *TimeoutError suite.Require().Nil(json.Unmarshal(b, &tErr2)) // Note that we cannot unmarshal retry reasons or inner error suite.Assert().Equal(tErr2.OperationID, err.OperationID) suite.Assert().Equal(tErr2.Opaque, err.Opaque) suite.Assert().Equal(tErr2.TimeObserved, err.TimeObserved) suite.Assert().Equal(tErr2.RetryAttempts, err.RetryAttempts) suite.Assert().Equal(tErr2.LastDispatchedTo, err.LastDispatchedTo) suite.Assert().Equal(tErr2.LastDispatchedFrom, err.LastDispatchedFrom) suite.Assert().Equal(tErr2.LastConnectionID, err.LastConnectionID) } func (suite *IntegrationTestSuite) TestTimeoutError_Retries() { suite.skipIfUnsupported(KeyValueFeature) var doc testBeerDocument err := loadJSONTestDataset("beer_sample_single", &doc) if err != nil { suite.T().Fatalf("Could not read test dataset: %v", err) } mutRes, err := globalCollection.Upsert("unlockTimeout", doc, nil) if err != nil { suite.T().Fatalf("Upsert failed, error was %v", err) } if mutRes.Cas() == 0 { suite.T().Fatalf("Upsert CAS was 0") } lockedDoc, err := globalCollection.GetAndLock("unlockTimeout", 10, nil) if err != nil { suite.T().Fatalf("Get failed, error was %v", err) } var lockedDocContent testBeerDocument err = lockedDoc.Content(&lockedDocContent) if err != nil { suite.T().Fatalf("Content failed, error was %v", err) } if doc != lockedDocContent { suite.T().Fatalf("Expected resulting doc to be %v but was %v", doc, lockedDocContent) } _, err = globalCollection.Upsert("unlockTimeout", 1234, &UpsertOptions{ Timeout: 1000 * time.Millisecond, }) if !errors.Is(err, ErrTimeout) { suite.T().Fatalf("Unlock should have errored with ErrTimeout but was %v", err) } var tErr *TimeoutError if errors.As(err, &tErr) { suite.Assert().Equal(tErr.OperationID, "Set") suite.Assert().NotEmpty(tErr.Opaque) // Testify doesn't like using Greater with time.Duration suite.Assert().Greater(tErr.TimeObserved.Microseconds(), 100*time.Millisecond.Microseconds()) suite.Assert().NotEmpty(tErr.RetryReasons) suite.Assert().Greater(tErr.RetryAttempts, uint32(0)) suite.Assert().NotEmpty(tErr.LastDispatchedTo) suite.Assert().NotEmpty(tErr.LastDispatchedFrom) suite.Assert().NotEmpty(tErr.LastConnectionID) } else { suite.T().Fatalf("Error couldn't be asserted to TimeoutError: %v", err) } } gocb-2.6.3/error_view.go000066400000000000000000000070111441755043100151510ustar00rootroot00000000000000package gocb import ( "encoding/json" gocbcore "github.com/couchbase/gocbcore/v10" ) // ViewErrorDesc represents a specific error returned from the views service. type ViewErrorDesc struct { SourceNode string Message string } func translateCoreViewErrorDesc(descs []gocbcore.ViewQueryErrorDesc) []ViewErrorDesc { descsOut := make([]ViewErrorDesc, len(descs)) for descIdx, desc := range descs { descsOut[descIdx] = ViewErrorDesc{ SourceNode: desc.SourceNode, Message: desc.Message, } } return descsOut } // ViewError is the error type of all view query errors. // UNCOMMITTED: This API may change in the future. type ViewError struct { InnerError error `json:"-"` DesignDocumentName string `json:"design_document_name,omitempty"` ViewName string `json:"view_name,omitempty"` Errors []ViewErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` } // MarshalJSON implements the Marshaler interface. func (e ViewError) MarshalJSON() ([]byte, error) { return json.Marshal(struct { InnerError string `json:"msg,omitempty"` DesignDocumentName string `json:"design_document_name,omitempty"` ViewName string `json:"view_name,omitempty"` Errors []ViewErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: e.InnerError.Error(), DesignDocumentName: e.DesignDocumentName, ViewName: e.ViewName, Errors: e.Errors, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, HTTPStatusCode: e.HTTPStatusCode, }) } // Error returns the string representation of this error. func (e ViewError) Error() string { errBytes, serErr := json.Marshal(struct { InnerError error `json:"-"` DesignDocumentName string `json:"design_document_name,omitempty"` ViewName string `json:"view_name,omitempty"` Errors []ViewErrorDesc `json:"errors,omitempty"` Endpoint string `json:"endpoint,omitempty"` RetryReasons []RetryReason `json:"retry_reasons,omitempty"` RetryAttempts uint32 `json:"retry_attempts,omitempty"` ErrorText string `json:"error_text,omitempty"` HTTPStatusCode int `json:"http_status_code,omitempty"` }{ InnerError: e.InnerError, DesignDocumentName: e.DesignDocumentName, ViewName: e.ViewName, Errors: e.Errors, Endpoint: e.Endpoint, RetryReasons: e.RetryReasons, RetryAttempts: e.RetryAttempts, ErrorText: e.ErrorText, HTTPStatusCode: e.HTTPStatusCode, }) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return e.InnerError.Error() + " | " + string(errBytes) } // Unwrap returns the underlying cause for this error. func (e ViewError) Unwrap() error { return e.InnerError } gocb-2.6.3/error_view_test.go000066400000000000000000000016731441755043100162200ustar00rootroot00000000000000package gocb import ( "encoding/json" ) func (suite *UnitTestSuite) TestViewError() { aErr := ViewError{ InnerError: ErrViewNotFound, DesignDocumentName: "designdoc", ViewName: "viewname", Errors: []ViewErrorDesc{ { SourceNode: "http://127.0.0.1:8092", Message: "error message", }, }, Endpoint: "http://127.0.0.1:8092", } b, err := json.Marshal(aErr) suite.Require().Nil(err) suite.Assert().Equal( []byte("{\"msg\":\"view not found\",\"design_document_name\":\"designdoc\",\"view_name\":\"viewname\",\"errors\":[{\"SourceNode\":\"http://127.0.0.1:8092\",\"Message\":\"error message\"}],\"endpoint\":\"http://127.0.0.1:8092\"}"), b, ) suite.Assert().Equal( "view not found | {\"design_document_name\":\"designdoc\",\"view_name\":\"viewname\",\"errors\":[{\"SourceNode\":\"http://127.0.0.1:8092\",\"Message\":\"error message\"}],\"endpoint\":\"http://127.0.0.1:8092\"}", aErr.Error(), ) } gocb-2.6.3/error_wrapping.go000066400000000000000000000122341441755043100160310ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" gocbcore "github.com/couchbase/gocbcore/v10" ) func serializeWrappedError(err error) string { errBytes, serErr := json.Marshal(err) if serErr != nil { logErrorf("failed to serialize error to json: %s", serErr.Error()) } return string(errBytes) } func maybeEnhanceCoreErr(err error) error { if kvErr, ok := err.(*gocbcore.KeyValueError); ok { return &KeyValueError{ InnerError: kvErr.InnerError, StatusCode: kvErr.StatusCode, DocumentID: kvErr.DocumentKey, BucketName: kvErr.BucketName, ScopeName: kvErr.ScopeName, CollectionName: kvErr.CollectionName, CollectionID: kvErr.CollectionID, ErrorName: kvErr.ErrorName, ErrorDescription: kvErr.ErrorDescription, Opaque: kvErr.Opaque, Context: kvErr.Context, Ref: kvErr.Ref, RetryReasons: translateCoreRetryReasons(kvErr.RetryReasons), RetryAttempts: kvErr.RetryAttempts, LastDispatchedTo: kvErr.LastDispatchedTo, LastDispatchedFrom: kvErr.LastDispatchedFrom, LastConnectionID: kvErr.LastConnectionID, } } if viewErr, ok := err.(*gocbcore.ViewError); ok { return &ViewError{ InnerError: viewErr.InnerError, DesignDocumentName: viewErr.DesignDocumentName, ViewName: viewErr.ViewName, Errors: translateCoreViewErrorDesc(viewErr.Errors), Endpoint: viewErr.Endpoint, RetryReasons: translateCoreRetryReasons(viewErr.RetryReasons), RetryAttempts: viewErr.RetryAttempts, ErrorText: viewErr.ErrorText, HTTPStatusCode: viewErr.HTTPResponseCode, } } if queryErr, ok := err.(*gocbcore.N1QLError); ok { inner := queryErr.InnerError if errors.Is(inner, ErrFeatureNotAvailable) { if len(queryErr.Errors) > 0 { desc := queryErr.Errors[0] // We replace the gocbcore wrapped inner feature not available error with our own to provide gocb // specific context for the user. if desc.Code == 1197 { inner = wrapError(ErrFeatureNotAvailable, "this server requires that scope.Query() is used rather than "+ "cluster.Query(), if this is a transaction then pass a Scope within TransactionQueryOptions") } } } return &QueryError{ InnerError: inner, Statement: queryErr.Statement, ClientContextID: queryErr.ClientContextID, Errors: translateCoreQueryErrorDesc(queryErr.Errors), Endpoint: queryErr.Endpoint, RetryReasons: translateCoreRetryReasons(queryErr.RetryReasons), RetryAttempts: queryErr.RetryAttempts, ErrorText: queryErr.ErrorText, HTTPStatusCode: queryErr.HTTPResponseCode, } } if analyticsErr, ok := err.(*gocbcore.AnalyticsError); ok { return &AnalyticsError{ InnerError: analyticsErr.InnerError, Statement: analyticsErr.Statement, ClientContextID: analyticsErr.ClientContextID, Errors: translateCoreAnalyticsErrorDesc(analyticsErr.Errors), Endpoint: analyticsErr.Endpoint, RetryReasons: translateCoreRetryReasons(analyticsErr.RetryReasons), RetryAttempts: analyticsErr.RetryAttempts, ErrorText: analyticsErr.ErrorText, HTTPStatusCode: analyticsErr.HTTPResponseCode, } } if searchErr, ok := err.(*gocbcore.SearchError); ok { return &SearchError{ InnerError: searchErr.InnerError, Query: searchErr.Query, Endpoint: searchErr.Endpoint, RetryReasons: translateCoreRetryReasons(searchErr.RetryReasons), RetryAttempts: searchErr.RetryAttempts, ErrorText: searchErr.ErrorText, IndexName: searchErr.IndexName, HTTPStatusCode: searchErr.HTTPResponseCode, } } if httpErr, ok := err.(*gocbcore.HTTPError); ok { return &HTTPError{ InnerError: httpErr.InnerError, UniqueID: httpErr.UniqueID, Endpoint: httpErr.Endpoint, RetryReasons: translateCoreRetryReasons(httpErr.RetryReasons), RetryAttempts: httpErr.RetryAttempts, } } if timeoutErr, ok := err.(*gocbcore.TimeoutError); ok { return &TimeoutError{ InnerError: timeoutErr.InnerError, OperationID: timeoutErr.OperationID, Opaque: timeoutErr.Opaque, TimeObserved: timeoutErr.TimeObserved, RetryReasons: translateCoreRetryReasons(timeoutErr.RetryReasons), RetryAttempts: timeoutErr.RetryAttempts, LastDispatchedTo: timeoutErr.LastDispatchedTo, LastDispatchedFrom: timeoutErr.LastDispatchedFrom, LastConnectionID: timeoutErr.LastConnectionID, } } return err } func maybeEnhanceKVErr(err error, bucketName, scopeName, collName, docKey string) error { return maybeEnhanceCoreErr(err) } func maybeEnhanceCollKVErr(err error, bucket kvProvider, coll *Collection, docKey string) error { return maybeEnhanceKVErr(err, coll.bucketName(), coll.Name(), coll.ScopeName(), docKey) } func maybeEnhanceViewError(err error) error { return maybeEnhanceCoreErr(err) } func maybeEnhanceQueryError(err error) error { return maybeEnhanceCoreErr(err) } func maybeEnhanceAnalyticsError(err error) error { return maybeEnhanceCoreErr(err) } func maybeEnhanceSearchError(err error) error { return maybeEnhanceCoreErr(err) } gocb-2.6.3/errors_transactions.go000066400000000000000000000141201441755043100170710ustar00rootroot00000000000000package gocb import ( "errors" "github.com/couchbase/gocbcore/v10" ) var ( // ErrOther indicates an non-specific error has occured. ErrOther = gocbcore.ErrOther // ErrTransient indicates a transient error occured which may succeed at a later point in time. ErrTransient = gocbcore.ErrTransient // ErrWriteWriteConflict indicates that another transaction conflicted with this one. ErrWriteWriteConflict = gocbcore.ErrWriteWriteConflict // ErrHard indicates that an unrecoverable error occured. ErrHard = gocbcore.ErrHard // ErrAmbiguous indicates that a failure occured but the outcome was not known. ErrAmbiguous = gocbcore.ErrAmbiguous // ErrAtrFull indicates that the ATR record was too full to accept a new mutation. ErrAtrFull = gocbcore.ErrAtrFull // ErrAttemptExpired indicates an transactionAttempt expired ErrAttemptExpired = gocbcore.ErrAttemptExpired // ErrAtrNotFound indicates that an expected ATR document was missing ErrAtrNotFound = gocbcore.ErrAtrNotFound // ErrAtrEntryNotFound indicates that an expected ATR entry was missing ErrAtrEntryNotFound = gocbcore.ErrAtrEntryNotFound // ErrDocAlreadyInTransaction indicates that a document is already in a transaction. ErrDocAlreadyInTransaction = gocbcore.ErrDocAlreadyInTransaction // ErrTransactionAbortedExternally indicates the transaction was aborted externally. ErrTransactionAbortedExternally = gocbcore.ErrTransactionAbortedExternally // ErrPreviousOperationFailed indicates a previous operation already failed. ErrPreviousOperationFailed = gocbcore.ErrPreviousOperationFailed // ErrForwardCompatibilityFailure indicates an operation failed due to involving a document in another transaction // which contains features this transaction does not support. ErrForwardCompatibilityFailure = gocbcore.ErrForwardCompatibilityFailure // ErrIllegalState is used for when a transaction enters an illegal State. ErrIllegalState = gocbcore.ErrIllegalState ErrAttemptNotFoundOnQuery = errors.New("transactionAttempt not found on query") ) type TransactionFailedError struct { cause error result *TransactionResult } func (tfe TransactionFailedError) Error() string { if tfe.cause == nil { return "transaction failed" } return "transaction failed | " + tfe.cause.Error() } func (tfe TransactionFailedError) Unwrap() error { return tfe.cause } // Internal: This should never be used and is not supported. func (tfe TransactionFailedError) Result() *TransactionResult { return tfe.result } type TransactionExpiredError struct { result *TransactionResult } func (tfe TransactionExpiredError) Error() string { return ErrAttemptExpired.Error() } func (tfe TransactionExpiredError) Unwrap() error { return ErrAttemptExpired } // Internal: This should never be used and is not supported. func (tfe TransactionExpiredError) Result() *TransactionResult { return tfe.result } type TransactionCommitAmbiguousError struct { cause error result *TransactionResult } func (tfe TransactionCommitAmbiguousError) Error() string { if tfe.cause == nil { return "transaction commit ambiguous" } return "transaction failed | " + tfe.cause.Error() } func (tfe TransactionCommitAmbiguousError) Unwrap() error { return tfe.cause } // Internal: This should never be used and is not supported. func (tfe TransactionCommitAmbiguousError) Result() *TransactionResult { return tfe.result } type TransactionFailedPostCommit struct { cause error result *TransactionResult } func (tfe TransactionFailedPostCommit) Error() string { if tfe.cause == nil { return "transaction failed post commit" } return "transaction failed | " + tfe.cause.Error() } func (tfe TransactionFailedPostCommit) Unwrap() error { return tfe.cause } // Internal: This should never be used and is not supported. func (tfe TransactionFailedPostCommit) Result() *TransactionResult { return tfe.result } // TransactionOperationFailedError is used when a transaction operation fails. // Internal: This should never be used and is not supported. type TransactionOperationFailedError struct { shouldRetry bool shouldNotRollback bool errorCause error shouldRaise gocbcore.TransactionErrorReason errorClass gocbcore.TransactionErrorClass } func (tfe TransactionOperationFailedError) Error() string { if tfe.errorCause == nil { return "transaction operation failed" } return "transaction operation failed | " + tfe.errorCause.Error() } // InternalUnwrap returns the underlying error for this error. func (tfe TransactionOperationFailedError) InternalUnwrap() error { return tfe.errorCause } // Retry signals whether a new transactionAttempt should be made at rollback. func (tfe TransactionOperationFailedError) Retry() bool { return tfe.shouldRetry } // Rollback signals whether the transactionAttempt should be auto-rolled back. func (tfe TransactionOperationFailedError) Rollback() bool { return !tfe.shouldNotRollback } // ToRaise signals which error type should be raised to the application. func (tfe TransactionOperationFailedError) ToRaise() TransactionErrorReason { return TransactionErrorReason(tfe.shouldRaise) } func createTransactionOperationFailedError(err error) error { if err == nil { return nil } var txnErr *gocbcore.TransactionOperationFailedError if errors.As(err, &txnErr) { return &TransactionOperationFailedError{ shouldRetry: txnErr.Retry(), shouldNotRollback: !txnErr.Rollback(), errorCause: txnErr.InternalUnwrap(), shouldRaise: txnErr.ToRaise(), errorClass: txnErr.ErrorClass(), } } return &TransactionOperationFailedError{ errorCause: err, errorClass: gocbcore.TransactionErrorClassFailOther, } } func errorReasonFromString(reason string) gocbcore.TransactionErrorReason { switch reason { case "failed": return gocbcore.TransactionErrorReasonTransactionFailed case "expired": return gocbcore.TransactionErrorReasonTransactionExpired case "commit_ambiguous": return gocbcore.TransactionErrorReasonTransactionCommitAmbiguous case "failed_post_commit": return gocbcore.TransactionErrorReasonTransactionFailedPostCommit default: return gocbcore.TransactionErrorReasonTransactionFailed } } gocb-2.6.3/errors_transactions_query.go000066400000000000000000000125721441755043100203270ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "github.com/couchbase/gocbcore/v10" ) func queryErrorCodeToError(code uint32, c *TransactionAttemptContext) error { switch code { case 1065: return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ErrorCause: ErrFeatureNotAvailable, }, c) case 1080: return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionExpired, ErrorCause: gocbcore.ErrAttemptExpired, ErrorClass: gocbcore.TransactionErrorClassFailExpiry, ShouldNotRollback: true, }, c) case 17004: return ErrAttemptNotFoundOnQuery case 17010: return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionExpired, ErrorCause: gocbcore.ErrAttemptExpired, ErrorClass: gocbcore.TransactionErrorClassFailExpiry, ShouldNotRollback: true, }, c) case 17012: return ErrDocumentExists case 17014: return ErrDocumentNotFound case 17015: return ErrCasMismatch default: return nil } } func queryCauseToOperationFailedError(queryErr *QueryError, c *TransactionAttemptContext) error { var operationFailedErrs []jsonQueryTransactionOperationFailedCause if err := json.Unmarshal([]byte(queryErr.ErrorText), &operationFailedErrs); err == nil { for _, operationFailedErr := range operationFailedErrs { if operationFailedErr.Cause != nil { if operationFailedErr.Code >= 17000 && operationFailedErr.Code <= 18000 { if err := queryErrorCodeToError(operationFailedErr.Code, c); err != nil { return err } } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: !operationFailedErr.Cause.Retry, ShouldNotRollback: !operationFailedErr.Cause.Rollback, Reason: errorReasonFromString(operationFailedErr.Cause.Raise), ErrorCause: queryErr, ShouldNotCommit: true, }, c) } } } return nil } func queryMaybeTranslateToTransactionsError(err error, c *TransactionAttemptContext) error { if errors.Is(err, ErrTimeout) { return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionExpired, ErrorCause: err, }, c) } var queryErr *QueryError if !errors.As(err, &queryErr) { return err } if len(queryErr.Errors) == 0 { return queryErr } // If an error contains a cause field, use that error. // Otherwise, if an error has code between 17000 and 18000 inclusive, it is a transactions-related error. Use that. // Otherwise, fallback to using the first error. if err := queryCauseToOperationFailedError(queryErr, c); err != nil { return err } for _, e := range queryErr.Errors { if e.Code >= 17000 && e.Code <= 18000 { if err := queryErrorCodeToError(e.Code, c); err != nil { return err } } } if err := queryErrorCodeToError(queryErr.Errors[0].Code, c); err != nil { return err } return queryErr } type transactionQueryOperationFailedDef struct { ShouldNotRetry bool ShouldNotRollback bool Reason gocbcore.TransactionErrorReason ErrorCause error ErrorClass gocbcore.TransactionErrorClass ShouldNotCommit bool } func operationFailed(def transactionQueryOperationFailedDef, c *TransactionAttemptContext) *TransactionOperationFailedError { err := &TransactionOperationFailedError{ shouldRetry: !def.ShouldNotRetry, shouldNotRollback: def.ShouldNotRollback, errorCause: def.ErrorCause, shouldRaise: def.Reason, errorClass: def.ErrorClass, } if c != nil { c.logger.logInfof(c.attemptID, "Operation failed: can still commit: %t, should not rollback: %t, should not retry: %t, "+ "reason: %s", !def.ShouldNotCommit, def.ShouldNotRollback, def.ShouldNotRetry, def.Reason) c.updateState(def) } return err } func (c *TransactionAttemptContext) updateState(def transactionQueryOperationFailedDef) { opts := gocbcore.TransactionUpdateStateOptions{} if def.ShouldNotRollback { opts.ShouldNotRollback = true } if def.ShouldNotRetry { opts.ShouldNotRetry = true } if def.ShouldNotCommit { opts.ShouldNotCommit = true } opts.Reason = def.Reason c.txn.UpdateState(opts) } func singleQueryErrToTransactionError(err error, txnID string) error { err = queryMaybeTranslateToTransactionsError(err, nil) var tErr *TransactionOperationFailedError if errors.As(err, &tErr) { switch tErr.shouldRaise { case gocbcore.TransactionErrorReasonTransactionFailed: return &TransactionFailedError{ cause: tErr.errorCause, result: &TransactionResult{ TransactionID: txnID, UnstagingComplete: false, }, } case gocbcore.TransactionErrorReasonTransactionCommitAmbiguous: return &TransactionCommitAmbiguousError{ cause: tErr.errorCause, result: &TransactionResult{ TransactionID: txnID, UnstagingComplete: false, }, } case gocbcore.TransactionErrorReasonTransactionExpired: return &TransactionExpiredError{ result: &TransactionResult{ TransactionID: txnID, UnstagingComplete: false, }, } } } return &TransactionFailedError{ cause: err, result: &TransactionResult{ TransactionID: txnID, UnstagingComplete: false, }, } } gocb-2.6.3/go.mod000066400000000000000000000003641441755043100135510ustar00rootroot00000000000000module github.com/couchbase/gocb/v2 require ( github.com/couchbase/gocbcore/v10 v10.2.3 github.com/couchbaselabs/gocaves/client v0.0.0-20230404095311-05e3ba4f0259 github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.8.2 ) go 1.13 gocb-2.6.3/go.sum000066400000000000000000000047021441755043100135760ustar00rootroot00000000000000github.com/couchbase/gocbcore/v10 v10.2.3 h1:PEkRSNSkKjUBXx82Ucr094+anoiCG5GleOOQZOHo6D4= github.com/couchbase/gocbcore/v10 v10.2.3/go.mod h1:lYQIIk+tzoMcwtwU5GzPbDdqEkwkH3isI2rkSpfL0oM= github.com/couchbaselabs/gocaves/client v0.0.0-20230307083111-cc3960c624b1/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY= github.com/couchbaselabs/gocaves/client v0.0.0-20230404095311-05e3ba4f0259 h1:2TXy68EGEzIMHOx9UvczR5ApVecwCfQZ0LjkmwMI6g4= github.com/couchbaselabs/gocaves/client v0.0.0-20230404095311-05e3ba4f0259/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gocb-2.6.3/kvopmanager.go000066400000000000000000000167161441755043100153140ustar00rootroot00000000000000package gocb import ( "context" "errors" "time" gocbcore "github.com/couchbase/gocbcore/v10" "github.com/couchbase/gocbcore/v10/memd" ) type kvOpManager struct { parent *Collection signal chan struct{} err error wasResolved bool mutationToken *MutationToken span RequestSpan documentID string transcoder Transcoder timeout time.Duration deadline time.Time bytes []byte flags uint32 persistTo uint replicateTo uint durabilityLevel memd.DurabilityLevel retryStrategy *retryStrategyWrapper cancelCh chan struct{} impersonate string operationName string createdTime time.Time meter *meterWrapper preserveTTL bool ctx context.Context } func (m *kvOpManager) getTimeout() time.Duration { if m.timeout > 0 { if m.durabilityLevel > 0 && m.timeout < durabilityTimeoutFloor { m.timeout = durabilityTimeoutFloor logWarnf("Durable operation in use so timeout value coerced up to %s", m.timeout.String()) } return m.timeout } defaultTimeout := m.parent.timeoutsConfig.KVTimeout if m.durabilityLevel > memd.DurabilityLevelMajority || m.persistTo > 0 { defaultTimeout = m.parent.timeoutsConfig.KVDurableTimeout } if m.durabilityLevel > 0 && defaultTimeout < durabilityTimeoutFloor { defaultTimeout = durabilityTimeoutFloor logWarnf("Durable operation in user so timeout value coerced up to %s", defaultTimeout.String()) } return defaultTimeout } func (m *kvOpManager) SetDocumentID(id string) { m.documentID = id } func (m *kvOpManager) SetCancelCh(cancelCh chan struct{}) { m.cancelCh = cancelCh } func (m *kvOpManager) SetTimeout(timeout time.Duration) { m.timeout = timeout } func (m *kvOpManager) SetTranscoder(transcoder Transcoder) { if transcoder == nil { transcoder = m.parent.transcoder } m.transcoder = transcoder } func (m *kvOpManager) SetValue(val interface{}) { if m.err != nil { return } if m.transcoder == nil { m.err = errors.New("Expected a transcoder to be specified first") return } espan := m.parent.startKvOpTrace("request_encoding", m.span.Context(), true) defer espan.End() bytes, flags, err := m.transcoder.Encode(val) if err != nil { m.err = err return } m.bytes = bytes m.flags = flags } func (m *kvOpManager) SetDuraOptions(persistTo, replicateTo uint, level DurabilityLevel) { if persistTo != 0 || replicateTo != 0 { if !m.parent.useMutationTokens { m.err = makeInvalidArgumentsError("cannot use observe based durability without mutation tokens") return } if level > 0 { m.err = makeInvalidArgumentsError("cannot mix observe based durability and synchronous durability") return } } if level == DurabilityLevelUnknown { level = DurabilityLevelNone } m.persistTo = persistTo m.replicateTo = replicateTo m.durabilityLevel, m.err = level.toMemd() if level > DurabilityLevelNone { levelStr, err := level.toManagementAPI() if err != nil { logDebugf("Could not convert durability level to string: %v", err) return } m.span.SetAttribute(spanAttribDBDurability, levelStr) } } func (m *kvOpManager) SetRetryStrategy(retryStrategy RetryStrategy) { wrapper := m.parent.retryStrategyWrapper if retryStrategy != nil { wrapper = newRetryStrategyWrapper(retryStrategy) } m.retryStrategy = wrapper } func (m *kvOpManager) SetImpersonate(user string) { m.impersonate = user } func (m *kvOpManager) SetContext(ctx context.Context) { if ctx == nil { ctx = context.Background() } m.ctx = ctx } func (m *kvOpManager) SetPreserveExpiry(preserveTTL bool) { m.preserveTTL = preserveTTL } func (m *kvOpManager) Finish(noMetrics bool) { m.span.End() if !noMetrics { m.meter.ValueRecord(meterValueServiceKV, m.operationName, m.createdTime) } } func (m *kvOpManager) TraceSpanContext() RequestSpanContext { return m.span.Context() } func (m *kvOpManager) TraceSpan() RequestSpan { return m.span } func (m *kvOpManager) DocumentID() []byte { return []byte(m.documentID) } func (m *kvOpManager) CollectionName() string { return m.parent.name() } func (m *kvOpManager) ScopeName() string { return m.parent.ScopeName() } func (m *kvOpManager) BucketName() string { return m.parent.bucketName() } func (m *kvOpManager) ValueBytes() []byte { return m.bytes } func (m *kvOpManager) ValueFlags() uint32 { return m.flags } func (m *kvOpManager) Transcoder() Transcoder { return m.transcoder } func (m *kvOpManager) DurabilityLevel() memd.DurabilityLevel { return m.durabilityLevel } func (m *kvOpManager) DurabilityTimeout() time.Duration { if m.durabilityLevel == 0 { return 0 } timeout := m.getTimeout() duraTimeout := time.Duration(float64(timeout) * 0.9) if duraTimeout < durabilityTimeoutFloor { duraTimeout = durabilityTimeoutFloor } return duraTimeout } func (m *kvOpManager) Deadline() time.Time { if m.deadline.IsZero() { timeout := m.getTimeout() m.deadline = time.Now().Add(timeout) } return m.deadline } func (m *kvOpManager) RetryStrategy() *retryStrategyWrapper { return m.retryStrategy } func (m *kvOpManager) Impersonate() string { return m.impersonate } func (m *kvOpManager) PreserveExpiry() bool { return m.preserveTTL } func (m *kvOpManager) CheckReadyForOp() error { if m.err != nil { return m.err } if m.getTimeout() == 0 { return errors.New("op manager had no timeout specified") } return nil } func (m *kvOpManager) NeedsObserve() bool { return m.persistTo > 0 || m.replicateTo > 0 } func (m *kvOpManager) EnhanceErr(err error) error { return maybeEnhanceCollKVErr(err, nil, m.parent, m.documentID) } func (m *kvOpManager) EnhanceMt(token gocbcore.MutationToken) *MutationToken { if token.VbUUID != 0 { return &MutationToken{ token: token, bucketName: m.BucketName(), } } return nil } func (m *kvOpManager) Reject() { m.signal <- struct{}{} } func (m *kvOpManager) Resolve(token *MutationToken) { m.wasResolved = true m.mutationToken = token m.signal <- struct{}{} } func (m *kvOpManager) Wait(op gocbcore.PendingOp, err error) error { if err != nil { return err } if m.err != nil { op.Cancel() } select { case <-m.signal: // Good to go case <-m.cancelCh: op.Cancel() <-m.signal case <-m.ctx.Done(): op.Cancel() <-m.signal } if m.wasResolved && (m.persistTo > 0 || m.replicateTo > 0) { if m.mutationToken == nil { return errors.New("expected a mutation token") } return m.parent.waitForDurability( m.ctx, m.span, m.documentID, m.mutationToken.token, m.replicateTo, m.persistTo, m.Deadline(), m.cancelCh, m.impersonate, ) } return nil } func (c *Collection) newKvOpManager(opName string, parentSpan RequestSpan) *kvOpManager { var tracectx RequestSpanContext if parentSpan != nil { tracectx = parentSpan.Context() } span := c.startKvOpTrace(opName, tracectx, false) return &kvOpManager{ parent: c, signal: make(chan struct{}, 1), span: span, operationName: opName, createdTime: time.Now(), meter: c.meter, } } func durationToExpiry(dura time.Duration) uint32 { // If the duration is 0, that indicates never-expires if dura == 0 { return 0 } // If the duration is less than one second, we must force the // value to 1 to avoid accidentally making it never expire. if dura < 1*time.Second { return 1 } if dura < 30*24*time.Hour { // Translate into a uint32 in seconds. return uint32(dura / time.Second) } // Send the duration as a unix timestamp of now plus duration. return uint32(time.Now().Add(dura).Unix()) } gocb-2.6.3/kvopmanager_test.go000066400000000000000000000156741441755043100163550ustar00rootroot00000000000000package gocb import ( "testing" "time" ) func (suite *IntegrationTestSuite) TestKvOpManagerTimeouts() { type tCase struct { name string timeout time.Duration durabilityLevel DurabilityLevel expectedDurabilityTimeout time.Duration expectedDeadline time.Duration } testCases := []tCase{ { name: "timeout", timeout: 3000 * time.Millisecond, expectedDurabilityTimeout: 0, expectedDeadline: 3000 * time.Millisecond, }, { name: "timeout, with durability level none", timeout: 3000 * time.Millisecond, durabilityLevel: DurabilityLevelNone, expectedDurabilityTimeout: 0, expectedDeadline: 3000 * time.Millisecond, }, { name: "timeout, with durability level majority", timeout: 3000 * time.Millisecond, durabilityLevel: DurabilityLevelMajority, expectedDurabilityTimeout: time.Duration(float64(3000*time.Millisecond) * 0.9), expectedDeadline: 3000 * time.Millisecond, }, { name: "timeout, with durability level persist to majority", timeout: 3000 * time.Millisecond, durabilityLevel: DurabilityLevelPersistToMajority, expectedDurabilityTimeout: time.Duration(float64(3000*time.Millisecond) * 0.9), expectedDeadline: 3000 * time.Millisecond, }, { name: "timeout, with durability level majority and persist master", timeout: 3000 * time.Millisecond, durabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, expectedDurabilityTimeout: time.Duration(float64(3000*time.Millisecond) * 0.9), expectedDeadline: 3000 * time.Millisecond, }, { name: "low timeout", timeout: 1000 * time.Millisecond, expectedDurabilityTimeout: 0, expectedDeadline: 1000 * time.Millisecond, }, { name: "low timeout, with durability level majority", timeout: 1000 * time.Millisecond, durabilityLevel: DurabilityLevelMajority, expectedDurabilityTimeout: durabilityTimeoutFloor, expectedDeadline: durabilityTimeoutFloor, }, { name: "low timeout, with durability level persist to majority", timeout: 1000 * time.Millisecond, durabilityLevel: DurabilityLevelPersistToMajority, expectedDurabilityTimeout: durabilityTimeoutFloor, expectedDeadline: durabilityTimeoutFloor, }, { name: "low timeout, with durability level majority and persist master", timeout: 1000 * time.Millisecond, durabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, expectedDurabilityTimeout: durabilityTimeoutFloor, expectedDeadline: durabilityTimeoutFloor, }, // Edge timeouts mean that the timeout set is above the durable floor but after applying the adaptive // algorithm the value will be below and require coercion. { name: "edge timeout", timeout: 1600 * time.Millisecond, expectedDurabilityTimeout: 0, expectedDeadline: 1600 * time.Millisecond, }, { name: "edge timeout, with durability level none", timeout: 1600 * time.Millisecond, durabilityLevel: DurabilityLevelNone, expectedDurabilityTimeout: 0, expectedDeadline: 1600 * time.Millisecond, }, { name: "edge timeout, with durability level majority", timeout: 1600 * time.Millisecond, durabilityLevel: DurabilityLevelMajority, expectedDurabilityTimeout: durabilityTimeoutFloor, expectedDeadline: 1600 * time.Millisecond, }, { name: "edge timeout, with durability level persist to majority", timeout: 1600 * time.Millisecond, durabilityLevel: DurabilityLevelPersistToMajority, expectedDurabilityTimeout: durabilityTimeoutFloor, expectedDeadline: 1600 * time.Millisecond, }, { name: "edge timeout, with durability level majority and persist master", timeout: 1600 * time.Millisecond, durabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, expectedDurabilityTimeout: durabilityTimeoutFloor, expectedDeadline: 1600 * time.Millisecond, }, { name: "no timeout", timeout: globalCollection.timeoutsConfig.KVTimeout, expectedDurabilityTimeout: 0, expectedDeadline: globalCollection.timeoutsConfig.KVTimeout, }, { name: "no timeout, with durability level none", timeout: globalCollection.timeoutsConfig.KVTimeout, durabilityLevel: DurabilityLevelNone, expectedDurabilityTimeout: 0, expectedDeadline: globalCollection.timeoutsConfig.KVTimeout, }, { name: "no timeout, with durability level majority", timeout: 0, durabilityLevel: DurabilityLevelMajority, expectedDurabilityTimeout: time.Duration(float64(globalCollection.timeoutsConfig.KVTimeout) * 0.9), expectedDeadline: globalCollection.timeoutsConfig.KVTimeout, }, { name: "no timeout, with durability level persist to majority", timeout: 0, durabilityLevel: DurabilityLevelPersistToMajority, expectedDurabilityTimeout: time.Duration(float64(globalCollection.timeoutsConfig.KVDurableTimeout) * 0.9), expectedDeadline: globalCollection.timeoutsConfig.KVDurableTimeout, }, { name: "no timeout, with durability level majority and persist master", timeout: 0, durabilityLevel: DurabilityLevelMajorityAndPersistOnMaster, expectedDurabilityTimeout: time.Duration(float64(globalCollection.timeoutsConfig.KVDurableTimeout) * 0.9), expectedDeadline: globalCollection.timeoutsConfig.KVDurableTimeout, }, } for _, tc := range testCases { suite.T().Run(tc.name, func(tt *testing.T) { mgr := globalCollection.newKvOpManager("test", nil) mgr.SetTimeout(tc.timeout) mgr.SetDuraOptions(0, 0, tc.durabilityLevel) deadline := mgr.Deadline() duraTimeout := mgr.DurabilityTimeout() diff := deadline.Sub(time.Now().Add(tc.expectedDeadline)) if diff > 5*time.Millisecond || diff < -5*time.Millisecond { tt.Logf("Expected deadline to be %s but was %s, not within 5ms delta", tc.expectedDeadline.String(), deadline.String()) tt.Fail() } if tc.expectedDurabilityTimeout != duraTimeout { tt.Logf("Expected durable timeout to be %s but was %s", tc.expectedDurabilityTimeout.String(), duraTimeout.String()) tt.Fail() } }) } } gocb-2.6.3/logging.go000066400000000000000000000107051441755043100144200ustar00rootroot00000000000000package gocb import ( "fmt" "log" "strings" gocbcore "github.com/couchbase/gocbcore/v10" ) // LogLevel specifies the severity of a log message. type LogLevel gocbcore.LogLevel // Various logging levels (or subsystems) which can categorize the message. // Currently these are ordered in decreasing severity. const ( LogError LogLevel = LogLevel(gocbcore.LogError) LogWarn LogLevel = LogLevel(gocbcore.LogWarn) LogInfo LogLevel = LogLevel(gocbcore.LogInfo) LogDebug LogLevel = LogLevel(gocbcore.LogDebug) LogTrace LogLevel = LogLevel(gocbcore.LogTrace) LogSched LogLevel = LogLevel(gocbcore.LogSched) LogMaxVerbosity LogLevel = LogLevel(gocbcore.LogMaxVerbosity) ) // LogRedactLevel specifies the degree with which to redact the logs. type LogRedactLevel uint const ( // RedactNone indicates to perform no redactions RedactNone LogRedactLevel = iota // RedactPartial indicates to redact all possible user-identifying information from logs. RedactPartial // RedactFull indicates to fully redact all possible identifying information from logs. RedactFull ) // SetLogRedactionLevel specifies the level with which logs should be redacted. func SetLogRedactionLevel(level LogRedactLevel) { globalLogRedactionLevel = level gocbcore.SetLogRedactionLevel(gocbcore.LogRedactLevel(level)) } func redactUserDataString(v string) string { return "" + v + "" } func redactSystemDataString(v string) string { return "" + v + "" } // Logger defines a logging interface. You can either use one of the default loggers // (DefaultStdioLogger(), VerboseStdioLogger()) or implement your own. type Logger interface { // Outputs logging information: // level is the verbosity level // offset is the position within the calling stack from which the message // originated. This is useful for contextual loggers which retrieve file/line // information. Log(level LogLevel, offset int, format string, v ...interface{}) error } var ( globalLogger Logger globalLogRedactionLevel LogRedactLevel ) type coreLogWrapper struct { wrapped gocbcore.Logger } func (wrapper coreLogWrapper) Log(level LogLevel, offset int, format string, v ...interface{}) error { return wrapper.wrapped.Log(gocbcore.LogLevel(level), offset+2, format, v...) } // DefaultStdioLogger gets the default standard I/O logger. // gocb.SetLogger(gocb.DefaultStdioLogger()) func DefaultStdioLogger() Logger { return &coreLogWrapper{ wrapped: gocbcore.DefaultStdioLogger(), } } // VerboseStdioLogger is a more verbose level of DefaultStdioLogger(). Messages // pertaining to the scheduling of ordinary commands (and their responses) will // also be emitted. // gocb.SetLogger(gocb.VerboseStdioLogger()) func VerboseStdioLogger() Logger { return coreLogWrapper{ wrapped: gocbcore.VerboseStdioLogger(), } } type coreLogger struct { wrapped Logger } func (wrapper coreLogger) Log(level gocbcore.LogLevel, offset int, format string, v ...interface{}) error { return wrapper.wrapped.Log(LogLevel(level), offset+2, format, v...) } func getCoreLogger(logger Logger) gocbcore.Logger { typedLogger, isCoreLogger := logger.(*coreLogWrapper) if isCoreLogger { return typedLogger.wrapped } return &coreLogger{ wrapped: logger, } } // SetLogger sets a logger to be used by the library. A logger can be obtained via // the DefaultStdioLogger() or VerboseStdioLogger() functions. You can also implement // your own logger using the Logger interface. func SetLogger(logger Logger) { globalLogger = logger gocbcore.SetLogger(getCoreLogger(logger)) // gocbcore.SetLogRedactionLevel(gocbcore.LogRedactLevel(globalLogRedactionLevel)) } func logExf(level LogLevel, offset int, format string, v ...interface{}) { if globalLogger != nil { err := globalLogger.Log(level, offset+1, format, v...) if err != nil { log.Printf("Logger error occurred (%s)\n", err) } } } func logInfof(format string, v ...interface{}) { logExf(LogInfo, 1, format, v...) } func logDebugf(format string, v ...interface{}) { logExf(LogDebug, 1, format, v...) } func logSchedf(format string, v ...interface{}) { logExf(LogSched, 1, format, v...) } func logWarnf(format string, v ...interface{}) { logExf(LogWarn, 1, format, v...) } func logErrorf(format string, v ...interface{}) { logExf(LogError, 1, format, v...) } func reindentLog(indent, message string) string { reindentedMessage := strings.Replace(message, "\n", "\n"+indent, -1) return fmt.Sprintf("%s%s", indent, reindentedMessage) } gocb-2.6.3/logging_meter.go000066400000000000000000000175211441755043100156170ustar00rootroot00000000000000package gocb import ( "bytes" "encoding/json" "fmt" "math" "sync" "sync/atomic" "time" ) type aggregatingMeterGroup struct { lock sync.Mutex recorders map[string]*aggregatingValueRecorder } func (amg *aggregatingMeterGroup) Recorders() []*aggregatingValueRecorder { amg.lock.Lock() if len(amg.recorders) == 0 { amg.lock.Unlock() return []*aggregatingValueRecorder{} } recorders := make([]*aggregatingValueRecorder, len(amg.recorders)) var i int for _, r := range amg.recorders { recorders[i] = r i++ } amg.lock.Unlock() return recorders } // LoggingMeter is a Meter implementation providing a simplified, but useful, view into current SDK state. type LoggingMeter struct { interval time.Duration valueRecorderGroups map[string]*aggregatingMeterGroup stopCh chan struct{} } // LoggingMeterOptions is the set of options available when creating a LoggingMeter. type LoggingMeterOptions struct { EmitInterval time.Duration } // NewLoggingMeter creates a new LoggingMeter. func NewLoggingMeter(opts *LoggingMeterOptions) *LoggingMeter { am := newAggregatingMeter(opts) am.startLoggerRoutine() return am } // AggregatingMeterOptions is the set of options available when creating a LoggingMeter. // Note that this function will soon be deprecated. // Deprecated: See LoggingMeterOptions. type AggregatingMeterOptions struct { EmitInterval time.Duration } // NewAggregatingMeter creates a new LoggingMeter. // Note that this function will soon be deprecated. // Deprecated: See NewLoggingMeter. func NewAggregatingMeter(opts *AggregatingMeterOptions) *LoggingMeter { am := newAggregatingMeter(&LoggingMeterOptions{ EmitInterval: opts.EmitInterval, }) am.startLoggerRoutine() return am } func newAggregatingMeter(opts *LoggingMeterOptions) *LoggingMeter { if opts == nil { opts = &LoggingMeterOptions{} } interval := opts.EmitInterval if interval == 0 { interval = 10 * time.Minute } am := &LoggingMeter{ interval: interval, valueRecorderGroups: map[string]*aggregatingMeterGroup{ meterValueServiceKV: { recorders: make(map[string]*aggregatingValueRecorder), }, meterValueServiceViews: { recorders: make(map[string]*aggregatingValueRecorder), }, meterValueServiceQuery: { recorders: make(map[string]*aggregatingValueRecorder), }, meterValueServiceSearch: { recorders: make(map[string]*aggregatingValueRecorder), }, meterValueServiceAnalytics: { recorders: make(map[string]*aggregatingValueRecorder), }, meterValueServiceManagement: { recorders: make(map[string]*aggregatingValueRecorder), }, }, stopCh: make(chan struct{}), } return am } func (am *LoggingMeter) startLoggerRoutine() { go am.loggerRoutine() } func (am *LoggingMeter) loggerRoutine() { for { select { case <-am.stopCh: return case <-time.After(am.interval): } jsonData := am.generateOutput() if len(jsonData) == 1 { // Nothing to log so make sure we don't just log empty objects. continue } // If we don't do this then json.Marshal will escape any < and > characters. jsonBytes := &bytes.Buffer{} encoder := json.NewEncoder(jsonBytes) encoder.SetEscapeHTML(false) err := encoder.Encode(jsonData) if err != nil { logDebugf("Failed to generate threshold logging service JSON: %s", err) } logInfof("Aggregate metrics: %s", jsonBytes) } } func (am *LoggingMeter) generateOutput() map[string]interface{} { output := make(map[string]interface{}) output["meta"] = map[string]interface{}{ "emit_interval_s": am.interval, } for serviceName, group := range am.valueRecorderGroups { serviceMap := make(map[string]interface{}) recorders := group.Recorders() if len(recorders) == 0 { continue } for _, recorder := range recorders { count, values := recorder.GetAndResetValues() // Don't log if there's nothing to log for this recorder. if count > 0 { serviceMap[recorder.operationName] = values } } if len(serviceMap) > 0 { output[serviceName] = serviceMap } } return output } func (am *LoggingMeter) Counter(_ string, _ map[string]string) (Counter, error) { return defaultNoopCounter, nil } func (am *LoggingMeter) ValueRecorder(name string, tags map[string]string) (ValueRecorder, error) { if name != meterNameCBOperations { return defaultNoopValueRecorder, nil } service, ok := tags[meterAttribServiceKey] if !ok { return defaultNoopValueRecorder, nil } if _, ok := am.valueRecorderGroups[service]; !ok { return defaultNoopValueRecorder, nil } operationName, ok := tags[meterAttribOperationKey] if !ok { return defaultNoopValueRecorder, nil } // We don't need to lock around accessing recorder groups itself, it must never be modified. recorderGroup := am.valueRecorderGroups[service] recorderGroup.lock.Lock() recorder := recorderGroup.recorders[operationName] if recorder == nil { recorder = newAggregatingValueRecorder(operationName) recorderGroup.recorders[operationName] = recorder } recorderGroup.lock.Unlock() return recorder, nil } func (am *LoggingMeter) close() { am.stopCh <- struct{}{} } type latencyHistogram struct { bins []uint64 maxValue float64 scaleFactor float64 ratioLog float64 commonRatio float64 startValue float64 } type cumulativeLatencyHistogram struct { bins []uint64 commonRatio float64 startValue float64 } func newLatencyHistogram(maxValue, startValue float64, commonRatio float64) *latencyHistogram { ratio := math.Log(commonRatio) // We plus two so that values > maxValue and values <= startValue will have a bin to go into numBuckets := math.Ceil(math.Log(maxValue/startValue)/ratio) + 2 return &latencyHistogram{ bins: make([]uint64, int(numBuckets)), maxValue: maxValue, scaleFactor: startValue, ratioLog: ratio, startValue: startValue, commonRatio: commonRatio, } } func (lh *latencyHistogram) RecordValue(value uint64) { var bin int v := float64(value) if v > lh.maxValue { bin = len(lh.bins) - 1 } else if v <= lh.scaleFactor { bin = 0 } else { bin = int(math.Ceil(math.Log(v/lh.scaleFactor) / lh.ratioLog)) } atomic.AddUint64(&lh.bins[bin], 1) } func (lh *latencyHistogram) AggregateAndReset() *cumulativeLatencyHistogram { bins := make([]uint64, len(lh.bins)) var countSoFar uint64 for i := 0; i < len(lh.bins); i++ { thisCount := atomic.SwapUint64(&lh.bins[i], 0) countSoFar += thisCount bins[i] = countSoFar } return &cumulativeLatencyHistogram{ bins: bins, commonRatio: lh.commonRatio, startValue: lh.startValue, } } func (lhs *cumulativeLatencyHistogram) TotalCount() uint64 { return lhs.bins[len(lhs.bins)-1] } func (lhs *cumulativeLatencyHistogram) BinAtPercentile(percentile float64) string { c := lhs.TotalCount() count := uint64(math.Ceil((percentile / 100) * float64(c))) for i, bin := range lhs.bins { if bin >= count { if i == len(lhs.bins)-1 { return fmt.Sprintf("> %.2f", math.Pow(lhs.commonRatio, float64(i-1))*lhs.startValue) } return fmt.Sprintf("<= %.2f", math.Pow(lhs.commonRatio, float64(i))*lhs.startValue) } } return "0.0" } type aggregatingValueRecorder struct { operationName string hist *latencyHistogram } func newAggregatingValueRecorder(operationName string) *aggregatingValueRecorder { return &aggregatingValueRecorder{ operationName: operationName, hist: newLatencyHistogram(2000000, 1000, 1.5), } } func (bc *aggregatingValueRecorder) RecordValue(val uint64) { bc.hist.RecordValue(val) } func (bc *aggregatingValueRecorder) GetAndResetValues() (uint64, map[string]interface{}) { hist := bc.hist.AggregateAndReset() c := hist.TotalCount() return c, map[string]interface{}{ "total_count": c, "percentiles_us": map[string]string{ "50.0": hist.BinAtPercentile(50.0), "90.0": hist.BinAtPercentile(90.0), "99.0": hist.BinAtPercentile(99.0), "99.9": hist.BinAtPercentile(99.9), "100.0": hist.BinAtPercentile(100), }, } } gocb-2.6.3/logging_meter_test.go000066400000000000000000000076031441755043100166560ustar00rootroot00000000000000package gocb import ( "time" ) func (suite *UnitTestSuite) TestLatencyHistogram() { histo := newLatencyHistogram(2000000, 1000, 1.5) suite.Require().Len(histo.bins, 21) histo.RecordValue(1000) histo.RecordValue(1000) histo.RecordValue(1000) histo.RecordValue(1000) histo.RecordValue(1000) histo.RecordValue(100000) histo.RecordValue(200000) histo.RecordValue(300000) histo.RecordValue(500000) histo.RecordValue(2000000) chisto := histo.AggregateAndReset() suite.Assert().Equal("<= 1000.00", chisto.BinAtPercentile(50)) suite.Assert().Equal("<= 129746.34", chisto.BinAtPercentile(60)) suite.Assert().Equal("<= 291929.26", chisto.BinAtPercentile(70)) suite.Assert().Equal("<= 437893.89", chisto.BinAtPercentile(80)) suite.Assert().Equal("<= 656840.84", chisto.BinAtPercentile(90)) suite.Assert().Equal("<= 2216837.82", chisto.BinAtPercentile(100)) } func (suite *UnitTestSuite) TestLatencyHistogramGreaterThanMax() { histo := newLatencyHistogram(2000000, 1000, 1.5) histo.RecordValue(4000000) chisto := histo.AggregateAndReset() suite.Assert().Equal("> 2216837.82", chisto.BinAtPercentile(100)) } func (suite *UnitTestSuite) TestAggregatingMeter() { meter := newAggregatingMeter(&LoggingMeterOptions{ EmitInterval: 10 * time.Second, }) r1, err := meter.ValueRecorder(meterNameCBOperations, map[string]string{ meterAttribServiceKey: "kv", meterAttribOperationKey: "get", }) suite.Require().Nil(err) r2, err := meter.ValueRecorder(meterNameCBOperations, map[string]string{ meterAttribServiceKey: "kv", meterAttribOperationKey: "replace", }) suite.Require().Nil(err) r3, err := meter.ValueRecorder(meterNameCBOperations, map[string]string{ meterAttribServiceKey: "query", meterAttribOperationKey: "query", }) suite.Require().Nil(err) r1.RecordValue(1000) r1.RecordValue(1000) r1.RecordValue(10000) r1.RecordValue(20000) r1.RecordValue(1500) r2.RecordValue(2000) r2.RecordValue(1000) r2.RecordValue(3500) r2.RecordValue(10000) r2.RecordValue(20000) r2.RecordValue(50000) r3.RecordValue(112000) output := meter.generateOutput() meta := output["meta"].(map[string]interface{}) suite.Assert().Equal(10*time.Second, meta["emit_interval_s"]) suite.Require().Contains(output, "kv") kvOutput := output["kv"].(map[string]interface{}) suite.Require().Contains(output, "query") queryOutput := output["query"].(map[string]interface{}) suite.Require().Contains(kvOutput, "get") suite.Require().Contains(queryOutput, "query") suite.Require().Contains(kvOutput, "replace") output1 := kvOutput["get"].(map[string]interface{}) output2 := kvOutput["replace"].(map[string]interface{}) qoutput := queryOutput["query"].(map[string]interface{}) suite.Assert().Equal(uint64(5), output1["total_count"]) suite.Assert().Equal(uint64(6), output2["total_count"]) suite.Assert().Equal(uint64(1), qoutput["total_count"]) percentiles1 := output1["percentiles_us"].(map[string]string) percentiles2 := output2["percentiles_us"].(map[string]string) percentilesq := qoutput["percentiles_us"].(map[string]string) suite.Assert().Equal("<= 1500.00", percentiles1["50.0"]) suite.Assert().Equal("<= 25628.91", percentiles1["90.0"]) suite.Assert().Equal("<= 25628.91", percentiles1["99.0"]) suite.Assert().Equal("<= 25628.91", percentiles1["99.9"]) suite.Assert().Equal("<= 25628.91", percentiles1["100.0"]) suite.Assert().Equal("<= 5062.50", percentiles2["50.0"]) suite.Assert().Equal("<= 57665.04", percentiles2["90.0"]) suite.Assert().Equal("<= 57665.04", percentiles2["99.0"]) suite.Assert().Equal("<= 57665.04", percentiles2["99.9"]) suite.Assert().Equal("<= 57665.04", percentiles2["100.0"]) suite.Assert().Equal("<= 129746.34", percentilesq["50.0"]) suite.Assert().Equal("<= 129746.34", percentilesq["90.0"]) suite.Assert().Equal("<= 129746.34", percentilesq["99.0"]) suite.Assert().Equal("<= 129746.34", percentilesq["99.9"]) suite.Assert().Equal("<= 129746.34", percentilesq["100.0"]) } gocb-2.6.3/metrics.go000066400000000000000000000075231441755043100144440ustar00rootroot00000000000000package gocb import ( "github.com/couchbase/gocbcore/v10" "sync" "time" ) // Meter handles metrics information for SDK operations. type Meter interface { Counter(name string, tags map[string]string) (Counter, error) ValueRecorder(name string, tags map[string]string) (ValueRecorder, error) } // Counter is used for incrementing a synchronous count metric. type Counter interface { IncrementBy(num uint64) } // ValueRecorder is used for grouping synchronous count metrics. type ValueRecorder interface { RecordValue(val uint64) } // NoopMeter is a Meter implementation which performs no metrics operations. type NoopMeter struct { } var ( defaultNoopCounter = &noopCounter{} defaultNoopValueRecorder = &noopValueRecorder{} ) // Counter is used for incrementing a synchronous count metric. func (nm *NoopMeter) Counter(name string, tags map[string]string) (Counter, error) { return defaultNoopCounter, nil } // ValueRecorder is used for grouping synchronous count metrics. func (nm *NoopMeter) ValueRecorder(name string, tags map[string]string) (ValueRecorder, error) { return defaultNoopValueRecorder, nil } type noopCounter struct{} func (bc *noopCounter) IncrementBy(num uint64) { } type noopValueRecorder struct{} func (bc *noopValueRecorder) RecordValue(val uint64) { } // nolint: unused type coreMeterWrapper struct { meter Meter } // nolint: unused func (meter *coreMeterWrapper) Counter(name string, tags map[string]string) (gocbcore.Counter, error) { counter, err := meter.meter.Counter(name, tags) if err != nil { return nil, err } return &coreCounterWrapper{ counter: counter, }, nil } // nolint: unused func (meter *coreMeterWrapper) ValueRecorder(name string, tags map[string]string) (gocbcore.ValueRecorder, error) { if name == "db.couchbase.requests" { // gocbcore has its own requests metrics, we don't want to record those. return &noopValueRecorder{}, nil } recorder, err := meter.meter.ValueRecorder(name, tags) if err != nil { return nil, err } return &coreValueRecorderWrapper{ valueRecorder: recorder, }, nil } // nolint: unused type coreCounterWrapper struct { counter Counter } // nolint: unused func (nm *coreCounterWrapper) IncrementBy(num uint64) { nm.counter.IncrementBy(num) } // nolint: unused type coreValueRecorderWrapper struct { valueRecorder ValueRecorder } // nolint: unused func (nm *coreValueRecorderWrapper) RecordValue(val uint64) { nm.valueRecorder.RecordValue(val) } type meterWrapper struct { attribsCache sync.Map meter Meter isNoopMeter bool } func newMeterWrapper(meter Meter) *meterWrapper { _, ok := meter.(*NoopMeter) return &meterWrapper{ meter: meter, isNoopMeter: ok, } } func (mw *meterWrapper) ValueRecorder(service, operation string) (ValueRecorder, error) { if mw.isNoopMeter { // If it's a noop meter then let's not pay the overhead of creating and caching attributes. return defaultNoopValueRecorder, nil } key := service + "." + operation attribs, ok := mw.attribsCache.Load(key) if !ok { // It doesn't really matter if we end up storing the attribs against the same key multiple times. We just need // to have a read efficient cache that doesn't cause actual data races. attribs = map[string]string{ meterAttribServiceKey: service, meterAttribOperationKey: operation, } mw.attribsCache.Store(key, attribs) } recorder, err := mw.meter.ValueRecorder(meterNameCBOperations, attribs.(map[string]string)) if err != nil { return nil, err } return recorder, nil } func (mw *meterWrapper) ValueRecord(service, operation string, start time.Time) { recorder, err := mw.ValueRecorder(service, operation) if err != nil { logDebugf("Failed to create value recorder: %v", err) return } duration := uint64(time.Since(start).Microseconds()) if duration == 0 { duration = uint64(1 * time.Microsecond) } recorder.RecordValue(duration) } gocb-2.6.3/metrics_test.go000066400000000000000000000027501441755043100155000ustar00rootroot00000000000000package gocb import ( "sync" "sync/atomic" ) type testCounter struct { count uint64 } func (tc *testCounter) IncrementBy(val uint64) { atomic.AddUint64(&tc.count, val) } type testValueRecorder struct { values []uint64 lock sync.Mutex } func (tvr *testValueRecorder) RecordValue(val uint64) { tvr.lock.Lock() tvr.values = append(tvr.values, val) tvr.lock.Unlock() } type testMeter struct { lock sync.Mutex counters map[string]*testCounter recorders map[string]*testValueRecorder } func newTestMeter() *testMeter { return &testMeter{ counters: make(map[string]*testCounter), recorders: make(map[string]*testValueRecorder), } } func (tm *testMeter) Reset() { tm.lock.Lock() tm.counters = make(map[string]*testCounter) tm.recorders = make(map[string]*testValueRecorder) tm.lock.Unlock() } func (tc *testMeter) Counter(name string, tags map[string]string) (Counter, error) { key := name + ":" + tags["db.operation"] tc.lock.Lock() counter := tc.counters[key] if counter == nil { counter = &testCounter{} tc.counters[key] = counter } tc.lock.Unlock() return counter, nil } func (tc *testMeter) ValueRecorder(name string, tags map[string]string) (ValueRecorder, error) { key := name + ":" + tags["db.couchbase.service"] if op, ok := tags["db.operation"]; ok { key = key + ":" + op } tc.lock.Lock() recorder := tc.recorders[key] if recorder == nil { recorder = &testValueRecorder{} tc.recorders[key] = recorder } tc.lock.Unlock() return recorder, nil } gocb-2.6.3/mgmt_http.go000066400000000000000000000065451441755043100150040ustar00rootroot00000000000000package gocb import ( "context" "io" "strings" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) type mgmtRequest struct { Service ServiceType Method string Path string Body []byte Headers map[string]string ContentType string IsIdempotent bool UniqueID string Timeout time.Duration RetryStrategy RetryStrategy parentSpanCtx RequestSpanContext } type mgmtResponse struct { Endpoint string StatusCode uint32 Body io.ReadCloser } type mgmtProvider interface { executeMgmtRequest(ctx context.Context, req mgmtRequest) (*mgmtResponse, error) } func (c *Cluster) executeMgmtRequest(ctx context.Context, req mgmtRequest) (mgmtRespOut *mgmtResponse, errOut error) { timeout := req.Timeout if timeout == 0 { timeout = c.timeoutsConfig.ManagementTimeout } provider, err := c.getHTTPProvider() if err != nil { return nil, err } retryStrategy := c.retryStrategyWrapper if req.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(req.RetryStrategy) } corereq := &gocbcore.HTTPRequest{ Service: gocbcore.ServiceType(req.Service), Method: req.Method, Path: req.Path, Body: req.Body, Headers: req.Headers, ContentType: req.ContentType, IsIdempotent: req.IsIdempotent, UniqueID: req.UniqueID, Deadline: time.Now().Add(timeout), RetryStrategy: retryStrategy, TraceContext: req.parentSpanCtx, } coreresp, err := provider.DoHTTPRequest(ctx, corereq) if err != nil { return nil, makeGenericHTTPError(err, corereq, coreresp) } resp := &mgmtResponse{ Endpoint: coreresp.Endpoint, StatusCode: uint32(coreresp.StatusCode), Body: coreresp.Body, } return resp, nil } func (b *Bucket) executeMgmtRequest(ctx context.Context, req mgmtRequest) (mgmtRespOut *mgmtResponse, errOut error) { timeout := req.Timeout if timeout == 0 { timeout = b.timeoutsConfig.ManagementTimeout } provider, err := b.connectionManager.getHTTPProvider(b.Name()) if err != nil { return nil, err } retryStrategy := b.retryStrategyWrapper if req.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(req.RetryStrategy) } corereq := &gocbcore.HTTPRequest{ Service: gocbcore.ServiceType(req.Service), Method: req.Method, Path: req.Path, Body: req.Body, Headers: req.Headers, ContentType: req.ContentType, IsIdempotent: req.IsIdempotent, UniqueID: req.UniqueID, Deadline: time.Now().Add(timeout), RetryStrategy: retryStrategy, TraceContext: req.parentSpanCtx, } coreresp, err := provider.DoHTTPRequest(ctx, corereq) if err != nil { return nil, makeGenericHTTPError(err, corereq, coreresp) } resp := &mgmtResponse{ Endpoint: coreresp.Endpoint, StatusCode: uint32(coreresp.StatusCode), Body: coreresp.Body, } return resp, nil } func ensureBodyClosed(body io.ReadCloser) { err := body.Close() if err != nil { logDebugf("Failed to close socket: %v", err) } } func checkForRateLimitError(statusCode uint32, errMsg string) error { if statusCode != 429 { return nil } errMsg = strings.ToLower(errMsg) var err error if strings.Contains(errMsg, "limit(s) exceeded") { err = ErrRateLimitedFailure } else if strings.Contains(errMsg, "maximum number of collections has been reached for scope") { err = ErrQuotaLimitedFailure } return err } gocb-2.6.3/mock_analyticsProvider_test.go000066400000000000000000000027161441755043100205470ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockAnalyticsProvider is an autogenerated mock type for the analyticsProvider type type mockAnalyticsProvider struct { mock.Mock } // AnalyticsQuery provides a mock function with given fields: ctx, opts func (_m *mockAnalyticsProvider) AnalyticsQuery(ctx context.Context, opts gocbcore.AnalyticsQueryOptions) (analyticsRowReader, error) { ret := _m.Called(ctx, opts) var r0 analyticsRowReader if rf, ok := ret.Get(0).(func(context.Context, gocbcore.AnalyticsQueryOptions) analyticsRowReader); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(analyticsRowReader) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, gocbcore.AnalyticsQueryOptions) error); ok { r1 = rf(ctx, opts) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockAnalyticsProvider interface { mock.TestingT Cleanup(func()) } // newMockAnalyticsProvider creates a new instance of mockAnalyticsProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockAnalyticsProvider(t mockConstructorTestingTnewMockAnalyticsProvider) *mockAnalyticsProvider { mock := &mockAnalyticsProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_connectionManager_test.go000066400000000000000000000156131441755043100204770ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockConnectionManager is an autogenerated mock type for the connectionManager type type mockConnectionManager struct { mock.Mock } // buildConfig provides a mock function with given fields: cluster func (_m *mockConnectionManager) buildConfig(cluster *Cluster) error { ret := _m.Called(cluster) var r0 error if rf, ok := ret.Get(0).(func(*Cluster) error); ok { r0 = rf(cluster) } else { r0 = ret.Error(0) } return r0 } // close provides a mock function with given fields: func (_m *mockConnectionManager) close() error { ret := _m.Called() var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // connect provides a mock function with given fields: func (_m *mockConnectionManager) connect() error { ret := _m.Called() var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // connection provides a mock function with given fields: bucketName func (_m *mockConnectionManager) connection(bucketName string) (*gocbcore.Agent, error) { ret := _m.Called(bucketName) var r0 *gocbcore.Agent if rf, ok := ret.Get(0).(func(string) *gocbcore.Agent); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*gocbcore.Agent) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // getAnalyticsProvider provides a mock function with given fields: func (_m *mockConnectionManager) getAnalyticsProvider() (analyticsProvider, error) { ret := _m.Called() var r0 analyticsProvider if rf, ok := ret.Get(0).(func() analyticsProvider); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(analyticsProvider) } } var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { r1 = ret.Error(1) } return r0, r1 } // getDiagnosticsProvider provides a mock function with given fields: bucketName func (_m *mockConnectionManager) getDiagnosticsProvider(bucketName string) (diagnosticsProvider, error) { ret := _m.Called(bucketName) var r0 diagnosticsProvider if rf, ok := ret.Get(0).(func(string) diagnosticsProvider); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(diagnosticsProvider) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // getHTTPProvider provides a mock function with given fields: bucketName func (_m *mockConnectionManager) getHTTPProvider(bucketName string) (httpProvider, error) { ret := _m.Called(bucketName) var r0 httpProvider if rf, ok := ret.Get(0).(func(string) httpProvider); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(httpProvider) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // getKvCapabilitiesProvider provides a mock function with given fields: bucketName func (_m *mockConnectionManager) getKvCapabilitiesProvider(bucketName string) (kvCapabilityVerifier, error) { ret := _m.Called(bucketName) var r0 kvCapabilityVerifier if rf, ok := ret.Get(0).(func(string) kvCapabilityVerifier); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(kvCapabilityVerifier) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // getKvProvider provides a mock function with given fields: bucketName func (_m *mockConnectionManager) getKvProvider(bucketName string) (kvProvider, error) { ret := _m.Called(bucketName) var r0 kvProvider if rf, ok := ret.Get(0).(func(string) kvProvider); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(kvProvider) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // getQueryProvider provides a mock function with given fields: func (_m *mockConnectionManager) getQueryProvider() (queryProvider, error) { ret := _m.Called() var r0 queryProvider if rf, ok := ret.Get(0).(func() queryProvider); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(queryProvider) } } var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { r1 = ret.Error(1) } return r0, r1 } // getSearchProvider provides a mock function with given fields: func (_m *mockConnectionManager) getSearchProvider() (searchProvider, error) { ret := _m.Called() var r0 searchProvider if rf, ok := ret.Get(0).(func() searchProvider); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(searchProvider) } } var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { r1 = ret.Error(1) } return r0, r1 } // getViewProvider provides a mock function with given fields: bucketName func (_m *mockConnectionManager) getViewProvider(bucketName string) (viewProvider, error) { ret := _m.Called(bucketName) var r0 viewProvider if rf, ok := ret.Get(0).(func(string) viewProvider); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(viewProvider) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // getWaitUntilReadyProvider provides a mock function with given fields: bucketName func (_m *mockConnectionManager) getWaitUntilReadyProvider(bucketName string) (waitUntilReadyProvider, error) { ret := _m.Called(bucketName) var r0 waitUntilReadyProvider if rf, ok := ret.Get(0).(func(string) waitUntilReadyProvider); ok { r0 = rf(bucketName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(waitUntilReadyProvider) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(bucketName) } else { r1 = ret.Error(1) } return r0, r1 } // openBucket provides a mock function with given fields: bucketName func (_m *mockConnectionManager) openBucket(bucketName string) error { ret := _m.Called(bucketName) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(bucketName) } else { r0 = ret.Error(0) } return r0 } type mockConstructorTestingTnewMockConnectionManager interface { mock.TestingT Cleanup(func()) } // newMockConnectionManager creates a new instance of mockConnectionManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockConnectionManager(t mockConstructorTestingTnewMockConnectionManager) *mockConnectionManager { mock := &mockConnectionManager{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_diagnosticsProvider_test.go000066400000000000000000000040111441755043100210550ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockDiagnosticsProvider is an autogenerated mock type for the diagnosticsProvider type type mockDiagnosticsProvider struct { mock.Mock } // Diagnostics provides a mock function with given fields: opts func (_m *mockDiagnosticsProvider) Diagnostics(opts gocbcore.DiagnosticsOptions) (*gocbcore.DiagnosticInfo, error) { ret := _m.Called(opts) var r0 *gocbcore.DiagnosticInfo if rf, ok := ret.Get(0).(func(gocbcore.DiagnosticsOptions) *gocbcore.DiagnosticInfo); ok { r0 = rf(opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*gocbcore.DiagnosticInfo) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.DiagnosticsOptions) error); ok { r1 = rf(opts) } else { r1 = ret.Error(1) } return r0, r1 } // Ping provides a mock function with given fields: ctx, opts func (_m *mockDiagnosticsProvider) Ping(ctx context.Context, opts gocbcore.PingOptions) (*gocbcore.PingResult, error) { ret := _m.Called(ctx, opts) var r0 *gocbcore.PingResult if rf, ok := ret.Get(0).(func(context.Context, gocbcore.PingOptions) *gocbcore.PingResult); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*gocbcore.PingResult) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, gocbcore.PingOptions) error); ok { r1 = rf(ctx, opts) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockDiagnosticsProvider interface { mock.TestingT Cleanup(func()) } // newMockDiagnosticsProvider creates a new instance of mockDiagnosticsProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockDiagnosticsProvider(t mockConstructorTestingTnewMockDiagnosticsProvider) *mockDiagnosticsProvider { mock := &mockDiagnosticsProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_httpProvider_test.go000066400000000000000000000026051441755043100175340ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockHttpProvider is an autogenerated mock type for the httpProvider type type mockHttpProvider struct { mock.Mock } // DoHTTPRequest provides a mock function with given fields: ctx, req func (_m *mockHttpProvider) DoHTTPRequest(ctx context.Context, req *gocbcore.HTTPRequest) (*gocbcore.HTTPResponse, error) { ret := _m.Called(ctx, req) var r0 *gocbcore.HTTPResponse if rf, ok := ret.Get(0).(func(context.Context, *gocbcore.HTTPRequest) *gocbcore.HTTPResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*gocbcore.HTTPResponse) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, *gocbcore.HTTPRequest) error); ok { r1 = rf(ctx, req) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockHttpProvider interface { mock.TestingT Cleanup(func()) } // newMockHttpProvider creates a new instance of mockHttpProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockHttpProvider(t mockConstructorTestingTnewMockHttpProvider) *mockHttpProvider { mock := &mockHttpProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_kvCapabilityVerifier_test.go000066400000000000000000000024171441755043100211610ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockKvCapabilityVerifier is an autogenerated mock type for the kvCapabilityVerifier type type mockKvCapabilityVerifier struct { mock.Mock } // BucketCapabilityStatus provides a mock function with given fields: cap func (_m *mockKvCapabilityVerifier) BucketCapabilityStatus(cap gocbcore.BucketCapability) gocbcore.BucketCapabilityStatus { ret := _m.Called(cap) var r0 gocbcore.BucketCapabilityStatus if rf, ok := ret.Get(0).(func(gocbcore.BucketCapability) gocbcore.BucketCapabilityStatus); ok { r0 = rf(cap) } else { r0 = ret.Get(0).(gocbcore.BucketCapabilityStatus) } return r0 } type mockConstructorTestingTnewMockKvCapabilityVerifier interface { mock.TestingT Cleanup(func()) } // newMockKvCapabilityVerifier creates a new instance of mockKvCapabilityVerifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockKvCapabilityVerifier(t mockConstructorTestingTnewMockKvCapabilityVerifier) *mockKvCapabilityVerifier { mock := &mockKvCapabilityVerifier{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_kvProvider_test.go000066400000000000000000000376211441755043100172030ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" time "time" ) // mockKvProvider is an autogenerated mock type for the kvProvider type type mockKvProvider struct { mock.Mock } // Add provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Add(opts gocbcore.AddOptions, cb gocbcore.StoreCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.AddOptions, gocbcore.StoreCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.AddOptions, gocbcore.StoreCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Append provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Append(opts gocbcore.AdjoinOptions, cb gocbcore.AdjoinCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.AdjoinOptions, gocbcore.AdjoinCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.AdjoinOptions, gocbcore.AdjoinCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Decrement provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Decrement(opts gocbcore.CounterOptions, cb gocbcore.CounterCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.CounterOptions, gocbcore.CounterCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.CounterOptions, gocbcore.CounterCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Delete provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Delete(opts gocbcore.DeleteOptions, cb gocbcore.DeleteCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.DeleteOptions, gocbcore.DeleteCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.DeleteOptions, gocbcore.DeleteCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Get provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Get(opts gocbcore.GetOptions, cb gocbcore.GetCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.GetOptions, gocbcore.GetCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.GetOptions, gocbcore.GetCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // GetAndLock provides a mock function with given fields: opts, cb func (_m *mockKvProvider) GetAndLock(opts gocbcore.GetAndLockOptions, cb gocbcore.GetAndLockCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.GetAndLockOptions, gocbcore.GetAndLockCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.GetAndLockOptions, gocbcore.GetAndLockCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // GetAndTouch provides a mock function with given fields: opts, cb func (_m *mockKvProvider) GetAndTouch(opts gocbcore.GetAndTouchOptions, cb gocbcore.GetAndTouchCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.GetAndTouchOptions, gocbcore.GetAndTouchCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.GetAndTouchOptions, gocbcore.GetAndTouchCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // GetMeta provides a mock function with given fields: opts, cb func (_m *mockKvProvider) GetMeta(opts gocbcore.GetMetaOptions, cb gocbcore.GetMetaCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.GetMetaOptions, gocbcore.GetMetaCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.GetMetaOptions, gocbcore.GetMetaCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // GetOneReplica provides a mock function with given fields: opts, cb func (_m *mockKvProvider) GetOneReplica(opts gocbcore.GetOneReplicaOptions, cb gocbcore.GetReplicaCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.GetOneReplicaOptions, gocbcore.GetReplicaCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.GetOneReplicaOptions, gocbcore.GetReplicaCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Increment provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Increment(opts gocbcore.CounterOptions, cb gocbcore.CounterCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.CounterOptions, gocbcore.CounterCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.CounterOptions, gocbcore.CounterCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // LookupIn provides a mock function with given fields: opts, cb func (_m *mockKvProvider) LookupIn(opts gocbcore.LookupInOptions, cb gocbcore.LookupInCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.LookupInOptions, gocbcore.LookupInCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.LookupInOptions, gocbcore.LookupInCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // MutateIn provides a mock function with given fields: opts, cb func (_m *mockKvProvider) MutateIn(opts gocbcore.MutateInOptions, cb gocbcore.MutateInCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.MutateInOptions, gocbcore.MutateInCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.MutateInOptions, gocbcore.MutateInCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Observe provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Observe(opts gocbcore.ObserveOptions, cb gocbcore.ObserveCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.ObserveOptions, gocbcore.ObserveCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.ObserveOptions, gocbcore.ObserveCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // ObserveVb provides a mock function with given fields: opts, cb func (_m *mockKvProvider) ObserveVb(opts gocbcore.ObserveVbOptions, cb gocbcore.ObserveVbCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.ObserveVbOptions, gocbcore.ObserveVbCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.ObserveVbOptions, gocbcore.ObserveVbCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Prepend provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Prepend(opts gocbcore.AdjoinOptions, cb gocbcore.AdjoinCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.AdjoinOptions, gocbcore.AdjoinCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.AdjoinOptions, gocbcore.AdjoinCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // RangeScanCancel provides a mock function with given fields: scanUUID, vbID, opts, cb func (_m *mockKvProvider) RangeScanCancel(scanUUID []byte, vbID uint16, opts gocbcore.RangeScanCancelOptions, cb gocbcore.RangeScanCancelCallback) (gocbcore.PendingOp, error) { ret := _m.Called(scanUUID, vbID, opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func([]byte, uint16, gocbcore.RangeScanCancelOptions, gocbcore.RangeScanCancelCallback) gocbcore.PendingOp); ok { r0 = rf(scanUUID, vbID, opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func([]byte, uint16, gocbcore.RangeScanCancelOptions, gocbcore.RangeScanCancelCallback) error); ok { r1 = rf(scanUUID, vbID, opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // RangeScanContinue provides a mock function with given fields: scanUUID, vbID, opts, dataCb, actionCb func (_m *mockKvProvider) RangeScanContinue(scanUUID []byte, vbID uint16, opts gocbcore.RangeScanContinueOptions, dataCb gocbcore.RangeScanContinueDataCallback, actionCb gocbcore.RangeScanContinueActionCallback) (gocbcore.PendingOp, error) { ret := _m.Called(scanUUID, vbID, opts, dataCb, actionCb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func([]byte, uint16, gocbcore.RangeScanContinueOptions, gocbcore.RangeScanContinueDataCallback, gocbcore.RangeScanContinueActionCallback) gocbcore.PendingOp); ok { r0 = rf(scanUUID, vbID, opts, dataCb, actionCb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func([]byte, uint16, gocbcore.RangeScanContinueOptions, gocbcore.RangeScanContinueDataCallback, gocbcore.RangeScanContinueActionCallback) error); ok { r1 = rf(scanUUID, vbID, opts, dataCb, actionCb) } else { r1 = ret.Error(1) } return r0, r1 } // RangeScanCreate provides a mock function with given fields: vbID, opts, cb func (_m *mockKvProvider) RangeScanCreate(vbID uint16, opts gocbcore.RangeScanCreateOptions, cb gocbcore.RangeScanCreateCallback) (gocbcore.PendingOp, error) { ret := _m.Called(vbID, opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(uint16, gocbcore.RangeScanCreateOptions, gocbcore.RangeScanCreateCallback) gocbcore.PendingOp); ok { r0 = rf(vbID, opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(uint16, gocbcore.RangeScanCreateOptions, gocbcore.RangeScanCreateCallback) error); ok { r1 = rf(vbID, opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Replace provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Replace(opts gocbcore.ReplaceOptions, cb gocbcore.StoreCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.ReplaceOptions, gocbcore.StoreCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.ReplaceOptions, gocbcore.StoreCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Set provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Set(opts gocbcore.SetOptions, cb gocbcore.StoreCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.SetOptions, gocbcore.StoreCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.SetOptions, gocbcore.StoreCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Touch provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Touch(opts gocbcore.TouchOptions, cb gocbcore.TouchCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.TouchOptions, gocbcore.TouchCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.TouchOptions, gocbcore.TouchCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // Unlock provides a mock function with given fields: opts, cb func (_m *mockKvProvider) Unlock(opts gocbcore.UnlockOptions, cb gocbcore.UnlockCallback) (gocbcore.PendingOp, error) { ret := _m.Called(opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(gocbcore.UnlockOptions, gocbcore.UnlockCallback) gocbcore.PendingOp); ok { r0 = rf(opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(gocbcore.UnlockOptions, gocbcore.UnlockCallback) error); ok { r1 = rf(opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } // WaitForConfigSnapshot provides a mock function with given fields: deadline, opts, cb func (_m *mockKvProvider) WaitForConfigSnapshot(deadline time.Time, opts gocbcore.WaitForConfigSnapshotOptions, cb gocbcore.WaitForConfigSnapshotCallback) (gocbcore.PendingOp, error) { ret := _m.Called(deadline, opts, cb) var r0 gocbcore.PendingOp if rf, ok := ret.Get(0).(func(time.Time, gocbcore.WaitForConfigSnapshotOptions, gocbcore.WaitForConfigSnapshotCallback) gocbcore.PendingOp); ok { r0 = rf(deadline, opts, cb) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(gocbcore.PendingOp) } } var r1 error if rf, ok := ret.Get(1).(func(time.Time, gocbcore.WaitForConfigSnapshotOptions, gocbcore.WaitForConfigSnapshotCallback) error); ok { r1 = rf(deadline, opts, cb) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockKvProvider interface { mock.TestingT Cleanup(func()) } // newMockKvProvider creates a new instance of mockKvProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockKvProvider(t mockConstructorTestingTnewMockKvProvider) *mockKvProvider { mock := &mockKvProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_mgmtProvider_test.go000066400000000000000000000024371441755043100175240ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" mock "github.com/stretchr/testify/mock" ) // mockMgmtProvider is an autogenerated mock type for the mgmtProvider type type mockMgmtProvider struct { mock.Mock } // executeMgmtRequest provides a mock function with given fields: ctx, req func (_m *mockMgmtProvider) executeMgmtRequest(ctx context.Context, req mgmtRequest) (*mgmtResponse, error) { ret := _m.Called(ctx, req) var r0 *mgmtResponse if rf, ok := ret.Get(0).(func(context.Context, mgmtRequest) *mgmtResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*mgmtResponse) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, mgmtRequest) error); ok { r1 = rf(ctx, req) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockMgmtProvider interface { mock.TestingT Cleanup(func()) } // newMockMgmtProvider creates a new instance of mockMgmtProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockMgmtProvider(t mockConstructorTestingTnewMockMgmtProvider) *mockMgmtProvider { mock := &mockMgmtProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_pendingOp_test.go000066400000000000000000000002261441755043100167620ustar00rootroot00000000000000package gocb import "github.com/stretchr/testify/mock" type mockPendingOp struct { mock.Mock } func (_m *mockPendingOp) Cancel() { _m.Called() } gocb-2.6.3/mock_queryProvider_test.go000066400000000000000000000037531441755043100177270ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockQueryProvider is an autogenerated mock type for the queryProvider type type mockQueryProvider struct { mock.Mock } // N1QLQuery provides a mock function with given fields: ctx, opts func (_m *mockQueryProvider) N1QLQuery(ctx context.Context, opts gocbcore.N1QLQueryOptions) (queryRowReader, error) { ret := _m.Called(ctx, opts) var r0 queryRowReader if rf, ok := ret.Get(0).(func(context.Context, gocbcore.N1QLQueryOptions) queryRowReader); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(queryRowReader) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, gocbcore.N1QLQueryOptions) error); ok { r1 = rf(ctx, opts) } else { r1 = ret.Error(1) } return r0, r1 } // PreparedN1QLQuery provides a mock function with given fields: ctx, opts func (_m *mockQueryProvider) PreparedN1QLQuery(ctx context.Context, opts gocbcore.N1QLQueryOptions) (queryRowReader, error) { ret := _m.Called(ctx, opts) var r0 queryRowReader if rf, ok := ret.Get(0).(func(context.Context, gocbcore.N1QLQueryOptions) queryRowReader); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(queryRowReader) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, gocbcore.N1QLQueryOptions) error); ok { r1 = rf(ctx, opts) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockQueryProvider interface { mock.TestingT Cleanup(func()) } // newMockQueryProvider creates a new instance of mockQueryProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockQueryProvider(t mockConstructorTestingTnewMockQueryProvider) *mockQueryProvider { mock := &mockQueryProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_searchProvider_test.go000066400000000000000000000026221441755043100200210ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockSearchProvider is an autogenerated mock type for the searchProvider type type mockSearchProvider struct { mock.Mock } // SearchQuery provides a mock function with given fields: ctx, opts func (_m *mockSearchProvider) SearchQuery(ctx context.Context, opts gocbcore.SearchQueryOptions) (searchRowReader, error) { ret := _m.Called(ctx, opts) var r0 searchRowReader if rf, ok := ret.Get(0).(func(context.Context, gocbcore.SearchQueryOptions) searchRowReader); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(searchRowReader) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, gocbcore.SearchQueryOptions) error); ok { r1 = rf(ctx, opts) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockSearchProvider interface { mock.TestingT Cleanup(func()) } // newMockSearchProvider creates a new instance of mockSearchProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockSearchProvider(t mockConstructorTestingTnewMockSearchProvider) *mockSearchProvider { mock := &mockSearchProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_viewProvider_test.go000066400000000000000000000025521441755043100175300ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" ) // mockViewProvider is an autogenerated mock type for the viewProvider type type mockViewProvider struct { mock.Mock } // ViewQuery provides a mock function with given fields: ctx, opts func (_m *mockViewProvider) ViewQuery(ctx context.Context, opts gocbcore.ViewQueryOptions) (viewRowReader, error) { ret := _m.Called(ctx, opts) var r0 viewRowReader if rf, ok := ret.Get(0).(func(context.Context, gocbcore.ViewQueryOptions) viewRowReader); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(viewRowReader) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, gocbcore.ViewQueryOptions) error); ok { r1 = rf(ctx, opts) } else { r1 = ret.Error(1) } return r0, r1 } type mockConstructorTestingTnewMockViewProvider interface { mock.TestingT Cleanup(func()) } // newMockViewProvider creates a new instance of mockViewProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockViewProvider(t mockConstructorTestingTnewMockViewProvider) *mockViewProvider { mock := &mockViewProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/mock_waitUntilReadyProvider_test.go000066400000000000000000000025111441755043100215160ustar00rootroot00000000000000// Code generated by mockery v2.14.0. DO NOT EDIT. package gocb import ( context "context" gocbcore "github.com/couchbase/gocbcore/v10" mock "github.com/stretchr/testify/mock" time "time" ) // mockWaitUntilReadyProvider is an autogenerated mock type for the waitUntilReadyProvider type type mockWaitUntilReadyProvider struct { mock.Mock } // WaitUntilReady provides a mock function with given fields: ctx, deadline, opts func (_m *mockWaitUntilReadyProvider) WaitUntilReady(ctx context.Context, deadline time.Time, opts gocbcore.WaitUntilReadyOptions) error { ret := _m.Called(ctx, deadline, opts) var r0 error if rf, ok := ret.Get(0).(func(context.Context, time.Time, gocbcore.WaitUntilReadyOptions) error); ok { r0 = rf(ctx, deadline, opts) } else { r0 = ret.Error(0) } return r0 } type mockConstructorTestingTnewMockWaitUntilReadyProvider interface { mock.TestingT Cleanup(func()) } // newMockWaitUntilReadyProvider creates a new instance of mockWaitUntilReadyProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func newMockWaitUntilReadyProvider(t mockConstructorTestingTnewMockWaitUntilReadyProvider) *mockWaitUntilReadyProvider { mock := &mockWaitUntilReadyProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } gocb-2.6.3/nodeversion_test.go000066400000000000000000000065121441755043100163650ustar00rootroot00000000000000package gocb import ( "errors" "fmt" "strconv" "strings" ) type NodeVersion struct { Major int Minor int Patch int Build int Edition NodeEdition Modifier string IsMock bool } type NodeEdition int const ( CommunityNodeEdition = NodeEdition(1) EnterpriseNodeEdition = NodeEdition(2) ) func (v NodeVersion) Equal(ov NodeVersion) bool { if v.Major == ov.Major && v.Minor == ov.Minor && v.Patch == ov.Patch && v.Edition == ov.Edition && v.Modifier == ov.Modifier { return true } return false } func (v NodeVersion) Higher(ov NodeVersion) bool { if v.Major > ov.Major { return true } else if v.Major < ov.Major { return false } if v.Minor > ov.Minor { return true } else if v.Minor < ov.Minor { return false } if v.Patch > ov.Patch { return true } else if v.Patch < ov.Patch { return false } if v.Build > ov.Build { return true } else if v.Build < ov.Build { return false } if v.Edition > ov.Edition { return true } return false } func (v NodeVersion) Lower(ov NodeVersion) bool { return !v.Higher(ov) && !v.Equal(ov) } func newNodeVersion(version string, isMock bool) (*NodeVersion, error) { nodeVersion, err := nodeVersionFromString(version) if err != nil { return nil, err } nodeVersion.IsMock = isMock return nodeVersion, nil } func nodeVersionFromString(version string) (*NodeVersion, error) { vSplit := strings.Split(version, ".") lenSplit := len(vSplit) if lenSplit == 0 { return nil, fmt.Errorf("must provide at least a major version") } var err error nodeVersion := NodeVersion{} nodeVersion.Major, err = strconv.Atoi(vSplit[0]) if err != nil { return nil, fmt.Errorf("major version is not a valid integer") } if lenSplit == 1 { return &nodeVersion, nil } nodeVersion.Minor, err = strconv.Atoi(vSplit[1]) if err != nil { return nil, fmt.Errorf("minor version is not a valid integer") } if lenSplit == 2 { return &nodeVersion, nil } nodeBuild := strings.Split(vSplit[2], "-") nodeVersion.Patch, err = strconv.Atoi(nodeBuild[0]) if err != nil { return nil, fmt.Errorf("patch version is not a valid integer") } if len(nodeBuild) == 1 { return &nodeVersion, nil } buildEdition := strings.Split(nodeBuild[1], "-") nodeVersion.Build, err = strconv.Atoi(buildEdition[0]) if err != nil { edition, modifier, err := editionModifierFromString(buildEdition[0]) if err != nil { return nil, err } nodeVersion.Edition = edition nodeVersion.Modifier = modifier return &nodeVersion, nil } if len(buildEdition) == 1 { return &nodeVersion, nil } edition, modifier, err := editionModifierFromString(buildEdition[1]) if err != nil { return nil, err } nodeVersion.Edition = edition nodeVersion.Modifier = modifier return &nodeVersion, nil } func editionModifierFromString(editionModifier string) (NodeEdition, string, error) { split := strings.Split(editionModifier, "-") editionStr := strings.ToLower(split[0]) var edition NodeEdition var modifier string if editionStr == "enterprise" { edition = EnterpriseNodeEdition } else if editionStr == "community" { edition = CommunityNodeEdition } else if editionStr == "dp" { modifier = editionStr } else { return 0, "", errors.New("Unrecognised edition or modifier: " + editionStr) } if len(split) == 1 { return edition, modifier, nil } return edition, strings.ToLower(split[1]), nil } gocb-2.6.3/providers.go000066400000000000000000000135111441755043100150050ustar00rootroot00000000000000package gocb import ( "context" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) // NOTE: context in these provider functions can be passed as a nil value. // The async op manager will check for a nil context.Context, the context values should never be assumed to be non-nil. type httpProvider interface { DoHTTPRequest(ctx context.Context, req *gocbcore.HTTPRequest) (*gocbcore.HTTPResponse, error) } type viewProvider interface { ViewQuery(ctx context.Context, opts gocbcore.ViewQueryOptions) (viewRowReader, error) } type queryProvider interface { N1QLQuery(ctx context.Context, opts gocbcore.N1QLQueryOptions) (queryRowReader, error) PreparedN1QLQuery(ctx context.Context, opts gocbcore.N1QLQueryOptions) (queryRowReader, error) } type analyticsProvider interface { AnalyticsQuery(ctx context.Context, opts gocbcore.AnalyticsQueryOptions) (analyticsRowReader, error) } type searchProvider interface { SearchQuery(ctx context.Context, opts gocbcore.SearchQueryOptions) (searchRowReader, error) } type waitUntilReadyProvider interface { WaitUntilReady(ctx context.Context, deadline time.Time, opts gocbcore.WaitUntilReadyOptions) error } type gocbcoreWaitUntilReadyProvider interface { WaitUntilReady(deadline time.Time, opts gocbcore.WaitUntilReadyOptions, cb gocbcore.WaitUntilReadyCallback) (gocbcore.PendingOp, error) } type diagnosticsProvider interface { Diagnostics(opts gocbcore.DiagnosticsOptions) (*gocbcore.DiagnosticInfo, error) Ping(ctx context.Context, opts gocbcore.PingOptions) (*gocbcore.PingResult, error) } type gocbcoreDiagnosticsProvider interface { Diagnostics(opts gocbcore.DiagnosticsOptions) (*gocbcore.DiagnosticInfo, error) Ping(opts gocbcore.PingOptions, cb gocbcore.PingCallback) (gocbcore.PendingOp, error) } type gocbcoreHTTPProvider interface { DoHTTPRequest(req *gocbcore.HTTPRequest, cb gocbcore.DoHTTPRequestCallback) (gocbcore.PendingOp, error) } type waitUntilReadyProviderWrapper struct { provider gocbcoreWaitUntilReadyProvider } func (wpw *waitUntilReadyProviderWrapper) WaitUntilReady(ctx context.Context, deadline time.Time, opts gocbcore.WaitUntilReadyOptions) (errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(wpw.provider.WaitUntilReady(deadline, opts, func(res *gocbcore.WaitUntilReadyResult, err error) { if err != nil { errOut = err opm.Reject() return } opm.Resolve() })) if err != nil { errOut = err return } return } type diagnosticsProviderWrapper struct { provider gocbcoreDiagnosticsProvider } func (dpw *diagnosticsProviderWrapper) Diagnostics(opts gocbcore.DiagnosticsOptions) (*gocbcore.DiagnosticInfo, error) { return dpw.provider.Diagnostics(opts) } func (dpw *diagnosticsProviderWrapper) Ping(ctx context.Context, opts gocbcore.PingOptions) (pOut *gocbcore.PingResult, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(dpw.provider.Ping(opts, func(res *gocbcore.PingResult, err error) { if err != nil { errOut = err opm.Reject() return } pOut = res opm.Resolve() })) if err != nil { errOut = err } return } type httpProviderWrapper struct { provider gocbcoreHTTPProvider } func (hpw *httpProviderWrapper) DoHTTPRequest(ctx context.Context, req *gocbcore.HTTPRequest) (respOut *gocbcore.HTTPResponse, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(hpw.provider.DoHTTPRequest(req, func(res *gocbcore.HTTPResponse, err error) { if err != nil { errOut = err opm.Reject() return } respOut = res opm.Resolve() })) if err != nil { errOut = err } return } type analyticsProviderWrapper struct { provider *gocbcore.AgentGroup } func (apw *analyticsProviderWrapper) AnalyticsQuery(ctx context.Context, opts gocbcore.AnalyticsQueryOptions) (aOut analyticsRowReader, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(apw.provider.AnalyticsQuery(opts, func(reader *gocbcore.AnalyticsRowReader, err error) { if err != nil { errOut = err opm.Reject() return } aOut = reader opm.Resolve() })) if err != nil { errOut = err } return } type queryProviderWrapper struct { provider *gocbcore.AgentGroup } func (apw *queryProviderWrapper) N1QLQuery(ctx context.Context, opts gocbcore.N1QLQueryOptions) (qOut queryRowReader, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(apw.provider.N1QLQuery(opts, func(reader *gocbcore.N1QLRowReader, err error) { if err != nil { errOut = err opm.Reject() return } qOut = reader opm.Resolve() })) if err != nil { errOut = err } return } func (apw *queryProviderWrapper) PreparedN1QLQuery(ctx context.Context, opts gocbcore.N1QLQueryOptions) (qOut queryRowReader, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(apw.provider.PreparedN1QLQuery(opts, func(reader *gocbcore.N1QLRowReader, err error) { if err != nil { errOut = err opm.Reject() return } qOut = reader opm.Resolve() })) if err != nil { errOut = err } return } type searchProviderWrapper struct { provider *gocbcore.AgentGroup } func (apw *searchProviderWrapper) SearchQuery(ctx context.Context, opts gocbcore.SearchQueryOptions) (sOut searchRowReader, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(apw.provider.SearchQuery(opts, func(reader *gocbcore.SearchRowReader, err error) { if err != nil { errOut = err opm.Reject() return } sOut = reader opm.Resolve() })) if err != nil { errOut = err } return } type viewProviderWrapper struct { provider *gocbcore.Agent } func (apw *viewProviderWrapper) ViewQuery(ctx context.Context, opts gocbcore.ViewQueryOptions) (vOut viewRowReader, errOut error) { opm := newAsyncOpManager(ctx) err := opm.Wait(apw.provider.ViewQuery(opts, func(reader *gocbcore.ViewQueryRowReader, err error) { if err != nil { errOut = err opm.Reject() return } vOut = reader opm.Resolve() })) if err != nil { errOut = err } return } gocb-2.6.3/query_options.go000066400000000000000000000116471441755043100157200ustar00rootroot00000000000000package gocb import ( "context" "strconv" "strings" "time" "github.com/google/uuid" ) // QueryScanConsistency indicates the level of data consistency desired for a query. type QueryScanConsistency uint const ( // QueryScanConsistencyNotBounded indicates no data consistency is required. QueryScanConsistencyNotBounded QueryScanConsistency = iota + 1 // QueryScanConsistencyRequestPlus indicates that request-level data consistency is required. QueryScanConsistencyRequestPlus ) // QueryOptions represents the options available when executing a query. type QueryOptions struct { ScanConsistency QueryScanConsistency ConsistentWith *MutationState Profile QueryProfileMode // ScanCap is the maximum buffered channel size between the indexer connectionManager and the query service for index scans. ScanCap uint32 // PipelineBatch controls the number of items execution operators can batch for Fetch from the KV. PipelineBatch uint32 // PipelineCap controls the maximum number of items each execution operator can buffer between various operators. PipelineCap uint32 // ScanWait is how long the indexer is allowed to wait until it can satisfy ScanConsistency/ConsistentWith criteria. ScanWait time.Duration Readonly bool // MaxParallelism is the maximum number of index partitions, for computing aggregation in parallel. MaxParallelism uint32 // ClientContextID provides a unique ID for this query which can be used matching up requests between connectionManager and // server. If not provided will be assigned a uuid value. ClientContextID string PositionalParameters []interface{} NamedParameters map[string]interface{} Metrics bool // Raw provides a way to provide extra parameters in the request body for the query. Raw map[string]interface{} Adhoc bool Timeout time.Duration RetryStrategy RetryStrategy // FlexIndex tells the query engine to use a flex index (utilizing the search service). FlexIndex bool // PreserveExpiry tells the query engine to preserve expiration values set on any documents modified by this query. PreserveExpiry bool ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // AsTransaction indicates to run this query as a transaction, providing any additional transaction specific // configuration. // UNCOMMITTED: This API may change in the future. AsTransaction *SingleQueryTransactionOptions // Internal: This should never be used and is not supported. Internal struct { User string Endpoint string } } func (opts *QueryOptions) toMap() (map[string]interface{}, error) { execOpts := make(map[string]interface{}) if opts.ScanConsistency != 0 && opts.ConsistentWith != nil { return nil, makeInvalidArgumentsError("ScanConsistency and ConsistentWith must be used exclusively") } if opts.ScanConsistency != 0 { if opts.ScanConsistency == QueryScanConsistencyNotBounded { execOpts["scan_consistency"] = "not_bounded" } else if opts.ScanConsistency == QueryScanConsistencyRequestPlus { execOpts["scan_consistency"] = "request_plus" } else { return nil, makeInvalidArgumentsError("Unexpected consistency option") } } if opts.ConsistentWith != nil { execOpts["scan_consistency"] = "at_plus" execOpts["scan_vectors"] = opts.ConsistentWith } if opts.Profile != "" { execOpts["profile"] = opts.Profile } if opts.Readonly { execOpts["readonly"] = opts.Readonly } if opts.PositionalParameters != nil && opts.NamedParameters != nil { return nil, makeInvalidArgumentsError("Positional and named parameters must be used exclusively") } if opts.PositionalParameters != nil { execOpts["args"] = opts.PositionalParameters } if opts.NamedParameters != nil { for key, value := range opts.NamedParameters { if !strings.HasPrefix(key, "$") { key = "$" + key } execOpts[key] = value } } if opts.ScanCap != 0 { execOpts["scan_cap"] = strconv.FormatUint(uint64(opts.ScanCap), 10) } if opts.PipelineBatch != 0 { execOpts["pipeline_batch"] = strconv.FormatUint(uint64(opts.PipelineBatch), 10) } if opts.PipelineCap != 0 { execOpts["pipeline_cap"] = strconv.FormatUint(uint64(opts.PipelineCap), 10) } if opts.ScanWait > 0 { execOpts["scan_wait"] = opts.ScanWait.String() } if opts.Raw != nil { for k, v := range opts.Raw { execOpts[k] = v } } if opts.MaxParallelism > 0 { execOpts["max_parallelism"] = strconv.FormatUint(uint64(opts.MaxParallelism), 10) } if !opts.Metrics { execOpts["metrics"] = false } if opts.ClientContextID == "" { execOpts["client_context_id"] = uuid.New() } else { execOpts["client_context_id"] = opts.ClientContextID } if opts.FlexIndex { execOpts["use_fts"] = true } if opts.PreserveExpiry { execOpts["preserve_expiry"] = true } return execOpts, nil } gocb-2.6.3/rangescanopmanager.go000066400000000000000000000426531441755043100166340ustar00rootroot00000000000000package gocb import ( "context" "encoding/hex" "errors" "io" "math/rand" "sync/atomic" "time" "github.com/couchbase/gocbcore/v10" ) const ( rangeScanDefaultItemLimit = 50 rangeScanDefaultBytesLimit = 15000 ) type rangeScanOpManager struct { err error ctx context.Context span RequestSpan transcoder Transcoder timeout time.Duration deadline time.Time retryStrategy *retryStrategyWrapper impersonate string cancelCh chan struct{} dataCh chan *ScanResultItem streams map[uint16]*rangeScanStream agent kvProvider createdTime time.Time meter *meterWrapper tracer RequestTracer defaultRetryStrategy *retryStrategyWrapper defaultTranscoder Transcoder defaultTimeout time.Duration collectionName string scopeName string bucketName string rangeOptions *gocbcore.RangeScanCreateRangeScanConfig samplingOptions *gocbcore.RangeScanCreateRandomSamplingConfig vBucketToSnapshotOpts map[uint16]gocbcore.RangeScanCreateSnapshotRequirements numVbuckets int keysOnly bool sort ScanSort itemLimit uint32 byteLimit uint32 result *ScanResult cancelled uint32 } func (m *rangeScanOpManager) getTimeout() time.Duration { if m.timeout > 0 { return m.timeout } return m.defaultTimeout } func (m *rangeScanOpManager) SetTimeout(timeout time.Duration) { m.timeout = timeout } func (m *rangeScanOpManager) SetItemLimit(limit uint32) { if limit == 0 { limit = rangeScanDefaultItemLimit } m.itemLimit = limit } func (m *rangeScanOpManager) SetByteLimit(limit uint32) { if limit == 0 { limit = rangeScanDefaultBytesLimit } m.byteLimit = limit } func (m *rangeScanOpManager) SetResult(result *ScanResult) { m.result = result } func (m *rangeScanOpManager) SetTranscoder(transcoder Transcoder) { if transcoder == nil { transcoder = m.defaultTranscoder } m.transcoder = transcoder } func (m *rangeScanOpManager) SetRetryStrategy(retryStrategy RetryStrategy) { wrapper := m.defaultRetryStrategy if retryStrategy != nil { wrapper = newRetryStrategyWrapper(retryStrategy) } m.retryStrategy = wrapper } func (m *rangeScanOpManager) SetImpersonate(user string) { m.impersonate = user } func (m *rangeScanOpManager) SetContext(ctx context.Context) { if ctx == nil { ctx = context.Background() } m.ctx = ctx } func (m *rangeScanOpManager) Finish() { m.span.End() m.meter.ValueRecord(meterValueServiceKV, "range_scan", m.createdTime) } func (m *rangeScanOpManager) TraceSpanContext() RequestSpanContext { return m.span.Context() } func (m *rangeScanOpManager) TraceSpan() RequestSpan { return m.span } func (m *rangeScanOpManager) CollectionName() string { return m.collectionName } func (m *rangeScanOpManager) ScopeName() string { return m.scopeName } func (m *rangeScanOpManager) BucketName() string { return m.bucketName } func (m *rangeScanOpManager) Transcoder() Transcoder { return m.transcoder } func (m *rangeScanOpManager) RangeOptions() *gocbcore.RangeScanCreateRangeScanConfig { return m.rangeOptions } func (m *rangeScanOpManager) SamplingOptions() *gocbcore.RangeScanCreateRandomSamplingConfig { return m.samplingOptions } func (m *rangeScanOpManager) SnapshotOptions(vbID uint16) *gocbcore.RangeScanCreateSnapshotRequirements { opts, ok := m.vBucketToSnapshotOpts[vbID] if !ok { return nil } return &opts } func (m *rangeScanOpManager) KeysOnly() bool { return m.keysOnly } func (m *rangeScanOpManager) CheckReadyForOp() error { if m.err != nil { return m.err } timeout := m.getTimeout() if timeout == 0 { return errors.New("range scan op manager had no timeout specified") } m.deadline = time.Now().Add(timeout) return nil } func (m *rangeScanOpManager) EnhanceErr(err error) error { return maybeEnhanceKVErr(err, m.bucketName, m.scopeName, m.collectionName, "scan") } func (m *rangeScanOpManager) Deadline() time.Time { return m.deadline } func (m *rangeScanOpManager) Timeout() time.Duration { return m.getTimeout() } func (m *rangeScanOpManager) RetryStrategy() *retryStrategyWrapper { return m.retryStrategy } func (m *rangeScanOpManager) Impersonate() string { return m.impersonate } func (m *rangeScanOpManager) Context() context.Context { return m.ctx } // Cancel will trigger all underlying streams to cancel themselves, the read loop // inside of Scan will handle calling Finish on the span and tidying up. func (m *rangeScanOpManager) Cancel() { m.cancel(ErrRequestCanceled) } func (m *rangeScanOpManager) cancel(err error) { if atomic.CompareAndSwapUint32(&m.cancelled, 0, 1) { m.result.setErr(err) close(m.cancelCh) } } func (m *rangeScanOpManager) DataCh() chan *ScanResultItem { return m.dataCh } func (m *rangeScanOpManager) getNextItemSorted() *ScanResultItem { var lowestKey string var lowestVbID uint16 for vbID, stream := range m.streams { peeked := stream.Peek() if peeked == nil { delete(m.streams, vbID) continue } if lowestKey == "" || peeked.id < lowestKey { lowestKey = peeked.id lowestVbID = vbID } } if lowestKey == "" { return nil } return m.streams[lowestVbID].Take() } func (m *rangeScanOpManager) getNextItem() *ScanResultItem { for vbID, stream := range m.streams { peeked := stream.Peek() if peeked == nil { delete(m.streams, vbID) continue } return stream.Take() } return nil } func (m *rangeScanOpManager) Scan() (*ScanResult, error) { var limit uint64 if m.SamplingOptions() != nil { limit = m.SamplingOptions().Samples } var numItems uint64 // Keep a track of the result object so that we can tell it any errors that occur. r := &ScanResult{ resultChan: m.DataCh(), cancelFn: m.Cancel, } m.SetResult(r) for vbucket := 0; vbucket < m.numVbuckets; vbucket++ { stream := m.newRangeScanStream(uint16(vbucket)) m.streams[uint16(vbucket)] = stream go func(stream *rangeScanStream) { // This may seem a little unusual but calling end only once scan has returned allows us to // avoid a lot of races that would otherwise be an issue. stream.Scan() stream.End() }(stream) } for _, stream := range m.streams { select { case <-stream.createdCh: // This stream is ready to go. case <-m.cancelCh: return nil, m.result.Err() } } go func() { for { var item *ScanResultItem if m.sort == ScanSortNone { item = m.getNextItem() } else { item = m.getNextItemSorted() } // If we're doing a sampling scan then we need to only write data into the channel // if we haven't seen the number of items that the user requested. Otherwise // we need to cancel the streams and iterate over them until they close. if item != nil && (limit == 0 || numItems < limit) { numItems++ m.dataCh <- item } if limit > 0 && numItems == limit { m.cancel(nil) } if len(m.streams) == 0 { m.Finish() close(m.dataCh) return } } }() return r, nil } func (c *Collection) newRangeScanOpManager(scanType ScanType, numVbuckets int, agent kvProvider, parentSpan RequestSpan, consistentWith *MutationState, keysOnly bool, sort ScanSort) (*rangeScanOpManager, error) { var tracectx RequestSpanContext if parentSpan != nil { tracectx = parentSpan.Context() } span := c.tracer.RequestSpan(tracectx, "range_scan") span.SetAttribute(spanAttribDBNameKey, c.bucket.Name()) span.SetAttribute(spanAttribDBCollectionNameKey, c.Name()) span.SetAttribute(spanAttribDBScopeNameKey, c.ScopeName()) span.SetAttribute(spanAttribServiceKey, "kv_scan") span.SetAttribute(spanAttribOperationKey, "range_scan") span.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) span.SetAttribute("num_partitions", numVbuckets) span.SetAttribute("without_content", keysOnly) var rangeOptions *gocbcore.RangeScanCreateRangeScanConfig var samplingOptions *gocbcore.RangeScanCreateRandomSamplingConfig setRangeScanOpts := func(st RangeScan) error { if st.To == nil { st.To = ScanTermMaximum() } if st.From == nil { st.From = ScanTermMinimum() } span.SetAttribute("scan_type", "range") span.SetAttribute("from_term", st.From.Term) span.SetAttribute("to_term", st.To.Term) var err error rangeOptions, err = st.toCore() if err != nil { return err } return nil } setSamplingScanOpts := func(st SamplingScan) error { if st.Seed == 0 { st.Seed = rand.Uint64() // #nosec G404 } span.SetAttribute("scan_type", "sampling") span.SetAttribute("limit", st.Limit) span.SetAttribute("seed", st.Seed) var err error samplingOptions, err = st.toCore() if err != nil { return err } return nil } var err error switch st := scanType.(type) { case RangeScan: if err := setRangeScanOpts(st); err != nil { return nil, err } case *RangeScan: if err := setRangeScanOpts(*st); err != nil { return nil, err } case SamplingScan: if err := setSamplingScanOpts(st); err != nil { return nil, err } case *SamplingScan: if err := setSamplingScanOpts(*st); err != nil { return nil, err } default: err = makeInvalidArgumentsError("only RangeScan and SamplingScan are supported for ScanType") } vBucketToSnapshotOpts := make(map[uint16]gocbcore.RangeScanCreateSnapshotRequirements) if consistentWith != nil { for _, token := range consistentWith.tokens { vBucketToSnapshotOpts[uint16(token.PartitionID())] = gocbcore.RangeScanCreateSnapshotRequirements{ VbUUID: gocbcore.VbUUID(token.PartitionUUID()), SeqNo: gocbcore.SeqNo(token.SequenceNumber()), } } } m := &rangeScanOpManager{ err: err, span: span, createdTime: time.Now(), meter: c.meter, tracer: c.tracer, dataCh: make(chan *ScanResultItem), cancelCh: make(chan struct{}), streams: make(map[uint16]*rangeScanStream, numVbuckets), numVbuckets: numVbuckets, agent: agent, defaultTimeout: c.timeoutsConfig.KVScanTimeout, defaultTranscoder: c.transcoder, defaultRetryStrategy: c.retryStrategyWrapper, collectionName: c.Name(), scopeName: c.ScopeName(), bucketName: c.Bucket().Name(), rangeOptions: rangeOptions, samplingOptions: samplingOptions, vBucketToSnapshotOpts: vBucketToSnapshotOpts, keysOnly: keysOnly, sort: sort, } return m, nil } type rangeScanStream struct { // This is a bit lazy, but it saves us copying all the information into 1024 more places. opm *rangeScanOpManager buffer chan ScanResultItem vbID uint16 peeked *ScanResultItem span RequestSpan createdCh chan struct{} } func (m *rangeScanOpManager) newRangeScanStream(vbID uint16) *rangeScanStream { span := m.tracer.RequestSpan(m.span.Context(), "range_scan_partition") span.SetAttribute("partition_id", vbID) return &rangeScanStream{ opm: m, buffer: make(chan ScanResultItem), vbID: vbID, span: span, createdCh: make(chan struct{}), } } func (rss *rangeScanStream) Take() *ScanResultItem { peeked := rss.peeked rss.peeked = nil return peeked } func (rss *rangeScanStream) Peek() *ScanResultItem { if rss.peeked != nil { return rss.peeked } select { case peeked, hasMore := <-rss.buffer: if !hasMore { return nil } rss.peeked = &peeked return rss.peeked case <-rss.opm.cancelCh: return nil } } func (rss *rangeScanStream) End() { rss.span.End() close(rss.buffer) } func (rss *rangeScanStream) Scan() { var lastTermSeen []byte rangeOpts := rss.opm.RangeOptions() samplingOpts := rss.opm.SamplingOptions() ctx := rss.opm.Context() var firstCreateDone bool for { if rangeOpts != nil && len(lastTermSeen) > 0 { rangeOpts.Start = lastTermSeen } scanUUID, err := rss.create(ctx, rangeOpts, samplingOpts) if err != nil { err = rss.opm.EnhanceErr(err) if errors.Is(err, gocbcore.ErrDocumentNotFound) { if !firstCreateDone { close(rss.createdCh) } logDebugf("Ignoring vbid %d as no documents exist for that vbucket", rss.vbID) return } // We only signal to cancel the entire stream if this is a range scan. if rangeOpts == nil { if !firstCreateDone { close(rss.createdCh) } } else { // We don't close the created channel here, because we don't want to signal a successful create // call to the stream manager. rss.opm.cancel(err) } return } if !firstCreateDone { close(rss.createdCh) } firstCreateDone = true // We only apply context to the initial create stream request, after that we consider the stream active // and context cancellation no longer applies. ctx = context.Background() // We've created the stream so now loop continue until the stream is complete or cancelled. for { items, isComplete, err := rss.scanContinue(scanUUID) if err != nil { err = rss.opm.EnhanceErr(err) // If the error is NMV or EOF then we should recreate the stream from the last known item. // Breaking here without calling cancel will trigger us to reloop rather than call Cancel on // the stream and then return. if errors.Is(err, gocbcore.ErrNotMyVBucket) || errors.Is(err, io.EOF) { break } rss.opm.cancel(err) break } if len(items) > 0 { for _, item := range items { var expiry time.Time if item.Expiry > 0 { expiry = time.Unix(int64(item.Expiry), 0) } select { case <-rss.opm.cancelCh: if !isComplete { rss.cancel(scanUUID) } return case rss.buffer <- ScanResultItem{ Result: Result{ cas: Cas(item.Cas), }, transcoder: rss.opm.Transcoder(), id: string(item.Key), flags: item.Flags, contents: item.Value, expiryTime: expiry, keysOnly: rss.opm.KeysOnly(), }: } } lastTermSeen = items[len(items)-1].Key } if isComplete { return } } select { case <-rss.opm.cancelCh: rss.cancel(scanUUID) return default: } } } func (rss *rangeScanStream) create(ctx context.Context, rangeOpts *gocbcore.RangeScanCreateRangeScanConfig, samplingOpts *gocbcore.RangeScanCreateRandomSamplingConfig) (uuidOut []byte, errOut error) { span := rss.opm.tracer.RequestSpan(rss.span.Context(), "range_scan_create") defer span.End() span.SetAttribute("without_content", rss.opm.KeysOnly()) if samplingOpts != nil { span.SetAttribute("scan_type", "sampling") span.SetAttribute("limit", samplingOpts.Samples) span.SetAttribute("seed", samplingOpts.Seed) } else if rangeOpts != nil { span.SetAttribute("scan_type", "range") span.SetAttribute("from_term", string(rangeOpts.Start)) span.SetAttribute("to_term", string(rangeOpts.End)) span.SetAttribute("from_exclusive", len(rangeOpts.ExclusiveStart) > 0) span.SetAttribute("to_exclusive", len(rangeOpts.ExclusiveEnd) > 0) } opMan := newAsyncOpManager(ctx) opMan.SetCancelCh(rss.opm.cancelCh) err := opMan.Wait(rss.opm.agent.RangeScanCreate(rss.vbID, gocbcore.RangeScanCreateOptions{ RetryStrategy: rss.opm.RetryStrategy(), Deadline: time.Now().Add(rss.opm.Timeout()), CollectionName: rss.opm.CollectionName(), ScopeName: rss.opm.ScopeName(), KeysOnly: rss.opm.KeysOnly(), Range: rangeOpts, Sampling: samplingOpts, Snapshot: rss.opm.SnapshotOptions(rss.vbID), User: rss.opm.Impersonate(), TraceContext: span.Context(), }, func(result *gocbcore.RangeScanCreateResult, err error) { if err != nil { errOut = err opMan.Reject() return } uuidOut = result.ScanUUUID opMan.Resolve() })) if err != nil { errOut = err } return } func (rss *rangeScanStream) scanContinue(scanUUID []byte) (itemsOut []gocbcore.RangeScanItem, completeOut bool, errOut error) { span := rss.opm.tracer.RequestSpan(rss.span.Context(), "range_scan_continue") defer span.End() span.SetAttribute("item_limit", rss.opm.itemLimit) span.SetAttribute("byte_limit", rss.opm.byteLimit) span.SetAttribute("time_limit", 0) opm := newAsyncOpManager(context.Background()) opm.SetCancelCh(rss.opm.cancelCh) var items []gocbcore.RangeScanItem span.SetAttribute("range_scan_id", "0x"+hex.EncodeToString(scanUUID)) err := opm.Wait(rss.opm.agent.RangeScanContinue(scanUUID, rss.vbID, gocbcore.RangeScanContinueOptions{ RetryStrategy: rss.opm.RetryStrategy(), User: rss.opm.Impersonate(), TraceContext: span.Context(), MaxCount: rss.opm.itemLimit, MaxBytes: rss.opm.byteLimit, }, func(coreItems []gocbcore.RangeScanItem) { items = append(items, coreItems...) }, func(result *gocbcore.RangeScanContinueResult, err error) { if err != nil { errOut = err opm.Reject() return } itemsOut = items if result.Complete { completeOut = true opm.Resolve() return } if result.More { opm.Resolve() return } logInfof("Received a range scan action that did not meet what we expected") opm.Resolve() })) if err != nil { errOut = err } return } func (rss *rangeScanStream) cancel(scanUUID []byte) { opMan := newAsyncOpManager(context.Background()) span := rss.opm.tracer.RequestSpan(rss.span.Context(), "range_scan_cancel") defer span.End() span.SetAttribute("range_scan_id", "0x"+hex.EncodeToString(scanUUID)) err := opMan.Wait(rss.opm.agent.RangeScanCancel(scanUUID, rss.vbID, gocbcore.RangeScanCancelOptions{ RetryStrategy: rss.opm.RetryStrategy(), Deadline: time.Now().Add(rss.opm.Timeout()), User: rss.opm.Impersonate(), TraceContext: rss.span.Context(), }, func(result *gocbcore.RangeScanCancelResult, err error) { if err != nil { logDebugf("Failed to cancel scan 0x%s: %v", hex.EncodeToString(scanUUID), err) opMan.Reject() return } opMan.Resolve() })) if err != nil { return } } gocb-2.6.3/results.go000066400000000000000000000256331441755043100145010ustar00rootroot00000000000000package gocb import ( "encoding/json" "sync" "time" ) // Result is the base type for the return types of operations type Result struct { cas Cas } // Cas returns the cas of the result. func (d *Result) Cas() Cas { return d.cas } // GetResult is the return type of Get operations. type GetResult struct { Result transcoder Transcoder flags uint32 contents []byte expiryTime *time.Time } // Content assigns the value of the result into the valuePtr using default decoding. func (d *GetResult) Content(valuePtr interface{}) error { return d.transcoder.Decode(d.contents, d.flags, valuePtr) } // Expiry returns the expiry value for the result if it available. Note that a nil // pointer indicates that the Expiry was not fetched, while a valid pointer to a zero // Duration indicates that the document will never expire. // Deprecated: Use ExpiryTime instead. func (d *GetResult) Expiry() *time.Duration { if d.expiryTime == nil { return nil } t := time.Until(*d.expiryTime) return &t } // ExpiryTime returns the expiry time for the result if it available. // This function will return a zero time if the value either was not fetched or the // document does not have an expiry time. func (d *GetResult) ExpiryTime() time.Time { if d.expiryTime == nil { return time.Time{} } return *d.expiryTime } func (d *GetResult) fromFullProjection(ops []LookupInSpec, result *LookupInResult, fields []string) error { if len(fields) == 0 { // This is a special case where user specified a full doc fetch with expiration. d.contents = result.contents[0].data return nil } if len(result.contents) != 1 { return makeInvalidArgumentsError("fromFullProjection should only be called with 1 subdoc result") } resultContent := result.contents[0] if resultContent.err != nil { return resultContent.err } var content map[string]interface{} err := json.Unmarshal(resultContent.data, &content) if err != nil { return err } newContent := make(map[string]interface{}) for _, field := range fields { parts := d.pathParts(field) d.set(parts, newContent, content[field]) } bytes, err := json.Marshal(newContent) if err != nil { return wrapError(err, "could not marshal result contents") } d.contents = bytes return nil } func (d *GetResult) fromSubDoc(ops []LookupInSpec, result *LookupInResult) error { content := make(map[string]interface{}) for i, op := range ops { err := result.contents[i].err if err != nil { // We return the first error that has occurred, this will be // a SubDocument error and will indicate the real reason. return err } parts := d.pathParts(op.path) d.set(parts, content, result.contents[i].data) } bytes, err := json.Marshal(content) if err != nil { return wrapError(err, "could not marshal result contents") } d.contents = bytes return nil } type subdocPath struct { path string isArray bool } func (d *GetResult) pathParts(pathStr string) []subdocPath { pathLen := len(pathStr) var elemIdx int var i int var paths []subdocPath for i < pathLen { ch := pathStr[i] i++ if ch == '[' { // opening of an array isArr := false arrayStart := i for i < pathLen { arrCh := pathStr[i] if arrCh == ']' { isArr = true i++ break } else if arrCh == '.' { i++ break } i++ } if isArr { paths = append(paths, subdocPath{path: pathStr[elemIdx : arrayStart-1], isArray: true}) } else { paths = append(paths, subdocPath{path: pathStr[elemIdx:i], isArray: false}) } elemIdx = i if i < pathLen && pathStr[i] == '.' { i++ elemIdx = i } } else if ch == '.' { paths = append(paths, subdocPath{path: pathStr[elemIdx : i-1]}) elemIdx = i } } if elemIdx != i { // this should only ever be an object as an array would have ended in [...] paths = append(paths, subdocPath{path: pathStr[elemIdx:i]}) } return paths } func (d *GetResult) set(paths []subdocPath, content interface{}, value interface{}) interface{} { path := paths[0] if len(paths) == 1 { if path.isArray { arr := make([]interface{}, 0) arr = append(arr, value) if _, ok := content.(map[string]interface{}); ok { content.(map[string]interface{})[path.path] = arr } else if _, ok := content.([]interface{}); ok { content = append(content.([]interface{}), arr) } else { logErrorf("Projections encountered a non-array or object content assigning an array") } } else { if _, ok := content.([]interface{}); ok { elem := make(map[string]interface{}) elem[path.path] = value content = append(content.([]interface{}), elem) } else { content.(map[string]interface{})[path.path] = value } } return content } if path.isArray { if _, ok := content.([]interface{}); ok { var m []interface{} content = append(content.([]interface{}), d.set(paths[1:], m, value)) return content } else if cMap, ok := content.(map[string]interface{}); ok { cMap[path.path] = make([]interface{}, 0) cMap[path.path] = d.set(paths[1:], cMap[path.path], value) return content } else { logErrorf("Projections encountered a non-array or object content assigning an array") } } else { if arr, ok := content.([]interface{}); ok { m := make(map[string]interface{}) m[path.path] = make(map[string]interface{}) content = append(arr, m) d.set(paths[1:], m[path.path], value) return content } cMap, ok := content.(map[string]interface{}) if !ok { // this isn't possible but the linter won't play nice without it logErrorf("Failed to assert projection content to a map") } cMap[path.path] = make(map[string]interface{}) return d.set(paths[1:], cMap[path.path], value) } return content } // LookupInResult is the return type for LookupIn. type LookupInResult struct { Result contents []lookupInPartial } type lookupInPartial struct { data json.RawMessage err error } func (pr *lookupInPartial) as(valuePtr interface{}) error { if pr.err != nil { return pr.err } if valuePtr == nil { return nil } if valuePtr, ok := valuePtr.(*[]byte); ok { *valuePtr = pr.data return nil } return json.Unmarshal(pr.data, valuePtr) } func (pr *lookupInPartial) exists() bool { err := pr.as(nil) return err == nil } // ContentAt retrieves the value of the operation by its index. The index is the position of // the operation as it was added to the builder. func (lir *LookupInResult) ContentAt(idx uint, valuePtr interface{}) error { if idx >= uint(len(lir.contents)) { return makeInvalidArgumentsError("invalid index") } return lir.contents[idx].as(valuePtr) } // Exists verifies that the item at idx exists. func (lir *LookupInResult) Exists(idx uint) bool { if idx >= uint(len(lir.contents)) { return false } return lir.contents[idx].exists() } // ExistsResult is the return type of Exist operations. type ExistsResult struct { Result docExists bool } // Exists returns whether or not the document exists. func (d *ExistsResult) Exists() bool { return d.docExists } // MutationResult is the return type of any store related operations. It contains Cas and mutation tokens. type MutationResult struct { Result mt *MutationToken } // MutationToken returns the mutation token belonging to an operation. func (mr MutationResult) MutationToken() *MutationToken { return mr.mt } // MutateInResult is the return type of any mutate in related operations. // It contains Cas, mutation tokens and any returned content. type MutateInResult struct { MutationResult contents []mutateInPartial } type mutateInPartial struct { data json.RawMessage } func (pr *mutateInPartial) as(valuePtr interface{}) error { if valuePtr == nil { return nil } if valuePtr, ok := valuePtr.(*[]byte); ok { *valuePtr = pr.data return nil } return json.Unmarshal(pr.data, valuePtr) } // ContentAt retrieves the value of the operation by its index. The index is the position of // the operation as it was added to the builder. func (mir MutateInResult) ContentAt(idx uint, valuePtr interface{}) error { return mir.contents[idx].as(valuePtr) } // CounterResult is the return type of counter operations. type CounterResult struct { MutationResult content uint64 } // MutationToken returns the mutation token belonging to an operation. func (mr CounterResult) MutationToken() *MutationToken { return mr.mt } // Cas returns the Cas value for a document following an operation. func (mr CounterResult) Cas() Cas { return mr.cas } // Content returns the new value for the counter document. func (mr CounterResult) Content() uint64 { return mr.content } // GetReplicaResult is the return type of GetReplica operations. type GetReplicaResult struct { GetResult isReplica bool } // IsReplica returns whether or not this result came from a replica server. func (r *GetReplicaResult) IsReplica() bool { return r.isReplica } // ScanResult is the return type of Scan operations. // VOLATILE: This API is subject to change at any time. type ScanResult struct { resultChan chan *ScanResultItem cancelFn func() err error errLock sync.Mutex } func (sr *ScanResult) setErr(err error) { sr.errLock.Lock() sr.err = err sr.errLock.Unlock() } // Next returns the next item on the stream, if there are no items remaining then nil is returned. func (sr *ScanResult) Next() *ScanResultItem { item, more := <-sr.resultChan if more { return item } return nil } // Err returns any errors that have occurred on the stream. func (sr *ScanResult) Err() error { sr.errLock.Lock() err := sr.err sr.errLock.Unlock() return err } // Close cancels the stream, returning any errors that occurred during reading the results. func (sr *ScanResult) Close() error { sr.errLock.Lock() err := sr.err sr.errLock.Unlock() if err != nil { return err } sr.cancelFn() return nil } // ScanResultItem represents an item that is returning on the stream from a Scan operation. type ScanResultItem struct { Result transcoder Transcoder id string flags uint32 contents []byte expiryTime time.Time keysOnly bool } // IDOnly returns whether the scan generating this item was made with IDsOnly set. func (sri *ScanResultItem) IDOnly() bool { return sri.keysOnly } // ID returns the id of the item. func (sri *ScanResultItem) ID() string { return sri.id } // Cas returns the Cas of the item. func (sri *ScanResultItem) Cas() Cas { return sri.cas } // Content assigns the value of the result into the valuePtr using default decoding. // If IDsOnly was set on the ScanOptions then this will return an error. func (sri *ScanResultItem) Content(valuePtr interface{}) error { if sri.keysOnly { return makeInvalidArgumentsError("scan was called with IDsOnly set to true, content can never be set") } return sri.transcoder.Decode(sri.contents, sri.flags, valuePtr) } // ExpiryTime returns the expiry time for the result if available. // This function will return a zero time if the value either was not fetched or the // document does not have an expiry time. func (sri *ScanResultItem) ExpiryTime() time.Time { return sri.expiryTime } gocb-2.6.3/results_test.go000066400000000000000000000214221441755043100155300ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "time" gocbcore "github.com/couchbase/gocbcore/v10" ) func (suite *UnitTestSuite) TestGetResultCas() { cas := Cas(10) res := GetResult{ Result: Result{ cas: cas, }, } if res.Cas() != cas { suite.T().Fatalf("Cas value should have been %d but was %d", cas, res.Cas()) } } func (suite *UnitTestSuite) TestGetResultExpiry() { res := GetResult{} suite.Require().Nil(res.Expiry()) suite.Require().Zero(res.ExpiryTime()) expiry := 32 * time.Second expiryTime := time.Now().Add(expiry) res.expiryTime = &expiryTime if suite.Assert().NotNil(res.Expiry()) { suite.Assert().InDelta(expiry, *res.Expiry(), float64(1*time.Second)) } suite.Assert().Equal(expiryTime, res.ExpiryTime()) } func (suite *UnitTestSuite) TestGetResultContent() { dataset, err := loadRawTestDataset("beer_sample_single") if err != nil { suite.T().Fatalf("Failed to load dataset: %v", err) } var expected testBeerDocument err = json.Unmarshal(dataset, &expected) if err != nil { suite.T().Fatalf("Failed to unmarshal dataset: %v", err) } res := GetResult{ contents: dataset, transcoder: NewJSONTranscoder(), } var doc testBeerDocument err = res.Content(&doc) if err != nil { suite.T().Fatalf("Failed to get content: %v", err) } // expected := "512_brewing_company (512) Bruin North American Ale" if doc != expected { suite.T().Fatalf("Document value should have been %+v but was %+v", expected, doc) } } func (suite *UnitTestSuite) TestGetResultFromSubDoc() { ops := []LookupInSpec{ { path: "id", }, { path: "name", }, { path: "address.house.number", }, } results := &LookupInResult{ contents: make([]lookupInPartial, 3), } var err error results.contents[0].data, err = json.Marshal("key") if err != nil { suite.T().Fatalf("Failed to marshal content: %v", err) } results.contents[1].data, err = json.Marshal("barry") if err != nil { suite.T().Fatalf("Failed to marshal content: %v", err) } results.contents[2].data, err = json.Marshal(11) if err != nil { suite.T().Fatalf("Failed to marshal content: %v", err) } type house struct { Number int `json:"number"` } type address struct { House house `json:"house"` } type person struct { ID string Name string Address address `json:"address"` } var doc person getResult := GetResult{transcoder: NewJSONTranscoder()} err = getResult.fromSubDoc(ops, results) if err != nil { suite.T().Fatalf("Failed to create result from subdoc: %v", err) } err = getResult.Content(&doc) if err != nil { suite.T().Fatalf("Failed to get content: %v", err) } if doc.ID != "key" { suite.T().Fatalf("Document value should have been %s but was %s", "key", doc.ID) } if doc.Name != "barry" { suite.T().Fatalf("Document value should have been %s but was %s", "barry", doc.ID) } if doc.Address.House.Number != 11 { suite.T().Fatalf("Document value should have been %d but was %d", 11, doc.Address.House.Number) } } func (suite *UnitTestSuite) TestLookupInResultCas() { cas := Cas(10) res := LookupInResult{ Result: Result{ cas: cas, }, } if res.Cas() != cas { suite.T().Fatalf("Cas value should have been %d but was %d", cas, res.Cas()) } } func (suite *UnitTestSuite) TestLookupInResultContentAt() { var dataset testBeerDocument err := loadJSONTestDataset("beer_sample_single", &dataset) if err != nil { suite.T().Fatalf("Failed to load dataset: %v", err) } contents1, err := json.Marshal(dataset.Name) if err != nil { suite.T().Fatalf("Failed to marshal data, %v", err) } contents2, err := json.Marshal(dataset.Description) if err != nil { suite.T().Fatalf("Failed to marshal data, %v", err) } type fakeBeer struct { Name string `json:"name"` } contentAsStruct := fakeBeer{ "beer", } contents3, err := json.Marshal(contentAsStruct) if err != nil { suite.T().Fatalf("Failed to marshal data, %v", err) } res := LookupInResult{ contents: []lookupInPartial{ { data: contents1, }, { data: contents2, }, { data: contents3, }, }, } var name string err = res.ContentAt(0, &name) if err != nil { suite.T().Fatalf("Failed to get contentat: %v", err) } if name != dataset.Name { suite.T().Fatalf("Name value should have been %s but was %s", dataset.Name, name) } if !res.Exists(0) { suite.T().Fatalf("Content value at 0 should have existed but didn't") } var description string err = res.ContentAt(1, &description) if err != nil { suite.T().Fatalf("Failed to get contentat: %v", err) } if description != dataset.Description { suite.T().Fatalf("Name value should have been %s but was %s", dataset.Description, description) } if !res.Exists(1) { suite.T().Fatalf("Content value at 1 should have existed but didn't") } var fake fakeBeer err = res.ContentAt(2, &fake) if err != nil { suite.T().Fatalf("Failed to get contentat: %v", err) } if fake != contentAsStruct { suite.T().Fatalf("Struct value should have been %v but was %v", contentAsStruct, fake) } if !res.Exists(2) { suite.T().Fatalf("Decode value at 2 should have existed but didn't") } var shouldFail string err = res.ContentAt(3, &shouldFail) if !errors.Is(err, ErrInvalidArgument) { suite.T().Fatalf("ContentAt should have failed with InvalidIndexError, was %v", err) } if res.Exists(3) { suite.T().Fatalf("Content value at 3 shouldn't have existed") } } func (suite *UnitTestSuite) TestExistsResultCas() { cas := Cas(10) res := ExistsResult{ Result: Result{ cas: Cas(cas), }, } if res.Cas() != cas { suite.T().Fatalf("Cas value should have been %d but was %d", cas, res.Cas()) } } func (suite *UnitTestSuite) TestExistsResultNotFound() { res := ExistsResult{ docExists: false, } if res.Exists() { suite.T().Fatalf("Expected result to not exist") } } func (suite *UnitTestSuite) TestExistsResultExists() { res := ExistsResult{ docExists: true, } if !res.Exists() { suite.T().Fatalf("Expected result to exist") } } func (suite *UnitTestSuite) TestMutationResultCas() { cas := Cas(10) res := MutationResult{ Result: Result{ cas: Cas(cas), }, } if res.Cas() != cas { suite.T().Fatalf("Cas value should have been %d but was %d", cas, res.Cas()) } } func (suite *UnitTestSuite) TestMutationResultMutationToken() { token := &MutationToken{ bucketName: "name", token: gocbcore.MutationToken{}, } res := MutationResult{ mt: token, } if res.MutationToken() != token { suite.T().Fatalf("Token value should have been %v but was %v", token, res.MutationToken()) } } func (suite *UnitTestSuite) TestCounterResultCas() { cas := Cas(10) res := CounterResult{ MutationResult: MutationResult{ Result: Result{ cas: Cas(cas), }, }, } if res.Cas() != cas { suite.T().Fatalf("Cas value should have been %d but was %d", cas, res.Cas()) } } func (suite *UnitTestSuite) TestCounterResultMutationToken() { token := &MutationToken{ bucketName: "name", token: gocbcore.MutationToken{}, } res := CounterResult{ MutationResult: MutationResult{ mt: token, }, } if res.MutationToken() != token { suite.T().Fatalf("Token value should have been %v but was %v", token, res.MutationToken()) } } func (suite *UnitTestSuite) TestCounterResultContent() { res := CounterResult{ content: 64, } if res.Content() != 64 { suite.T().Fatalf("Content value should have been %d but was %d", 64, res.Content()) } } func (suite *UnitTestSuite) TestMutateInResultCas() { cas := Cas(10) res := MutateInResult{ MutationResult: MutationResult{ Result: Result{ cas: Cas(cas), }, }, } if res.Cas() != cas { suite.T().Fatalf("Cas value should have been %d but was %d", cas, res.Cas()) } } func (suite *UnitTestSuite) TestMutateInResultMutationToken() { token := &MutationToken{ bucketName: "name", token: gocbcore.MutationToken{}, } res := MutateInResult{ MutationResult: MutationResult{ mt: token, }, } if res.MutationToken() != token { suite.T().Fatalf("Token value should have been %v but was %v", token, res.MutationToken()) } } func (suite *UnitTestSuite) TestMutateInResultContentAt() { results := &MutateInResult{ contents: make([]mutateInPartial, 2), } var err error results.contents[0].data, err = json.Marshal(23) if err != nil { suite.T().Fatalf("Failed to marshal content: %v", err) } results.contents[1].data, err = json.Marshal(1) if err != nil { suite.T().Fatalf("Failed to marshal content: %v", err) } var count int err = results.ContentAt(0, &count) if err != nil { suite.T().Fatalf("Failed to get contentat: %v", err) } if count != 23 { suite.T().Fatalf("Expected count to be %d but was %d", 23, count) } err = results.ContentAt(1, &count) if err != nil { suite.T().Fatalf("Failed to get contentat: %v", err) } if count != 1 { suite.T().Fatalf("Expected count to be %d but was %d", 1, count) } } gocb-2.6.3/retry.go000066400000000000000000000175571441755043100141530ustar00rootroot00000000000000package gocb import ( "time" "github.com/couchbase/gocbcore/v10" ) func translateCoreRetryReasons(reasons []gocbcore.RetryReason) []RetryReason { var reasonsOut []RetryReason for _, retryReason := range reasons { gocbReason, ok := retryReason.(RetryReason) if !ok { logErrorf("Failed to assert gocbcore retry reason to gocb retry reason: %v", retryReason) continue } reasonsOut = append(reasonsOut, gocbReason) } return reasonsOut } // RetryRequest is a request that can possibly be retried. type RetryRequest interface { RetryAttempts() uint32 Identifier() string Idempotent() bool RetryReasons() []RetryReason } type wrappedRetryRequest struct { req gocbcore.RetryRequest } func (req *wrappedRetryRequest) RetryAttempts() uint32 { return req.req.RetryAttempts() } func (req *wrappedRetryRequest) Identifier() string { return req.req.Identifier() } func (req *wrappedRetryRequest) Idempotent() bool { return req.req.Idempotent() } func (req *wrappedRetryRequest) RetryReasons() []RetryReason { return translateCoreRetryReasons(req.req.RetryReasons()) } // RetryReason represents the reason for an operation possibly being retried. type RetryReason interface { AllowsNonIdempotentRetry() bool AlwaysRetry() bool Description() string } var ( // UnknownRetryReason indicates that the operation failed for an unknown reason. UnknownRetryReason = RetryReason(gocbcore.UnknownRetryReason) // SocketNotAvailableRetryReason indicates that the operation failed because the underlying socket was not available. SocketNotAvailableRetryReason = RetryReason(gocbcore.SocketNotAvailableRetryReason) // ServiceNotAvailableRetryReason indicates that the operation failed because the requested service was not available. ServiceNotAvailableRetryReason = RetryReason(gocbcore.ServiceNotAvailableRetryReason) // NodeNotAvailableRetryReason indicates that the operation failed because the requested node was not available. NodeNotAvailableRetryReason = RetryReason(gocbcore.NodeNotAvailableRetryReason) // KVNotMyVBucketRetryReason indicates that the operation failed because it was sent to the wrong node for the vbucket. KVNotMyVBucketRetryReason = RetryReason(gocbcore.KVNotMyVBucketRetryReason) // KVCollectionOutdatedRetryReason indicates that the operation failed because the collection ID on the request is outdated. KVCollectionOutdatedRetryReason = RetryReason(gocbcore.KVCollectionOutdatedRetryReason) // KVErrMapRetryReason indicates that the operation failed for an unsupported reason but the KV error map indicated // that the operation can be retried. KVErrMapRetryReason = RetryReason(gocbcore.KVErrMapRetryReason) // KVLockedRetryReason indicates that the operation failed because the document was locked. KVLockedRetryReason = RetryReason(gocbcore.KVLockedRetryReason) // KVTemporaryFailureRetryReason indicates that the operation failed because of a temporary failure. KVTemporaryFailureRetryReason = RetryReason(gocbcore.KVTemporaryFailureRetryReason) // KVSyncWriteInProgressRetryReason indicates that the operation failed because a sync write is in progress. KVSyncWriteInProgressRetryReason = RetryReason(gocbcore.KVSyncWriteInProgressRetryReason) // KVSyncWriteRecommitInProgressRetryReason indicates that the operation failed because a sync write recommit is in progress. KVSyncWriteRecommitInProgressRetryReason = RetryReason(gocbcore.KVSyncWriteRecommitInProgressRetryReason) // ServiceResponseCodeIndicatedRetryReason indicates that the operation failed and the service responded stating that // the request should be retried. ServiceResponseCodeIndicatedRetryReason = RetryReason(gocbcore.ServiceResponseCodeIndicatedRetryReason) // SocketCloseInFlightRetryReason indicates that the operation failed because the socket was closed whilst the operation // was in flight. SocketCloseInFlightRetryReason = RetryReason(gocbcore.SocketCloseInFlightRetryReason) // CircuitBreakerOpenRetryReason indicates that the operation failed because the circuit breaker on the connection // was open. CircuitBreakerOpenRetryReason = RetryReason(gocbcore.CircuitBreakerOpenRetryReason) // QueryIndexNotFoundRetryReason indicates that the operation failed to to a missing query index QueryIndexNotFoundRetryReason = RetryReason(gocbcore.QueryIndexNotFoundRetryReason) // QueryPreparedStatementFailureRetryReason indicates that the operation failed due to a prepared statement failure QueryPreparedStatementFailureRetryReason = RetryReason(gocbcore.QueryPreparedStatementFailureRetryReason) // AnalyticsTemporaryFailureRetryReason indicates that an analytics operation failed due to a temporary failure AnalyticsTemporaryFailureRetryReason = RetryReason(gocbcore.AnalyticsTemporaryFailureRetryReason) // SearchTooManyRequestsRetryReason indicates that a search operation failed due to too many requests SearchTooManyRequestsRetryReason = RetryReason(gocbcore.SearchTooManyRequestsRetryReason) // QueryErrorRetryable indicates that the operation is retryable as indicated by the query engine. // Uncommitted: This API may change in the future. QueryErrorRetryable = RetryReason(gocbcore.QueryErrorRetryable) ) // RetryAction is used by a RetryStrategy to calculate the duration to wait before retrying an operation. // Returning a value of 0 indicates to not retry. type RetryAction interface { Duration() time.Duration } // NoRetryRetryAction represents an action that indicates to not retry. type NoRetryRetryAction struct { } // Duration is the length of time to wait before retrying an operation. func (ra *NoRetryRetryAction) Duration() time.Duration { return 0 } // WithDurationRetryAction represents an action that indicates to retry with a given duration. type WithDurationRetryAction struct { WithDuration time.Duration } // Duration is the length of time to wait before retrying an operation. func (ra *WithDurationRetryAction) Duration() time.Duration { return ra.WithDuration } // RetryStrategy is to determine if an operation should be retried, and if so how long to wait before retrying. type RetryStrategy interface { RetryAfter(req RetryRequest, reason RetryReason) RetryAction } func newRetryStrategyWrapper(strategy RetryStrategy) *retryStrategyWrapper { return &retryStrategyWrapper{ wrapped: strategy, } } type retryStrategyWrapper struct { wrapped RetryStrategy } // RetryAfter calculates and returns a RetryAction describing how long to wait before retrying an operation. func (rs *retryStrategyWrapper) RetryAfter(req gocbcore.RetryRequest, reason gocbcore.RetryReason) gocbcore.RetryAction { wreq := &wrappedRetryRequest{ req: req, } wrappedAction := rs.wrapped.RetryAfter(wreq, RetryReason(reason)) return gocbcore.RetryAction(wrappedAction) } // BackoffCalculator defines how backoff durations will be calculated by the retry API. type BackoffCalculator func(retryAttempts uint32) time.Duration // BestEffortRetryStrategy represents a strategy that will keep retrying until it succeeds (or the caller times out // the request). type BestEffortRetryStrategy struct { BackoffCalculator BackoffCalculator } // NewBestEffortRetryStrategy returns a new BestEffortRetryStrategy which will use the supplied calculator function // to calculate retry durations. If calculator is nil then a controlled backoff will be used. func NewBestEffortRetryStrategy(calculator BackoffCalculator) *BestEffortRetryStrategy { if calculator == nil { calculator = BackoffCalculator(gocbcore.ExponentialBackoff(1*time.Millisecond, 500*time.Millisecond, 2)) } return &BestEffortRetryStrategy{BackoffCalculator: calculator} } // RetryAfter calculates and returns a RetryAction describing how long to wait before retrying an operation. func (rs *BestEffortRetryStrategy) RetryAfter(req RetryRequest, reason RetryReason) RetryAction { if req.Idempotent() || reason.AllowsNonIdempotentRetry() { return &WithDurationRetryAction{WithDuration: rs.BackoffCalculator(req.RetryAttempts())} } return &NoRetryRetryAction{} } gocb-2.6.3/retry_test.go000066400000000000000000000131021441755043100151700ustar00rootroot00000000000000package gocb import ( "time" "github.com/couchbase/gocbcore/v10" ) // failFastRetryStrategy represents a strategy that will never retry. type failFastRetryStrategy struct { } // newFailFastRetryStrategy returns a new FailFastRetryStrategy. func newFailFastRetryStrategy() *failFastRetryStrategy { return &failFastRetryStrategy{} } // RetryAfter calculates and returns a RetryAction describing how long to wait before retrying an operation. func (rs *failFastRetryStrategy) RetryAfter(req RetryRequest, reason RetryReason) RetryAction { return &NoRetryRetryAction{} } type mockGocbcoreRequest struct { attempts uint32 identifier string idempotent bool reasons []gocbcore.RetryReason cancelSet bool gocbcore.RetryRequest } func (mgr *mockGocbcoreRequest) RetryAttempts() uint32 { return mgr.attempts } func (mgr *mockGocbcoreRequest) Identifier() string { return mgr.identifier } func (mgr *mockGocbcoreRequest) Idempotent() bool { return mgr.idempotent } func (mgr *mockGocbcoreRequest) RetryReasons() []gocbcore.RetryReason { return mgr.reasons } type mockRetryRequest struct { attempts uint32 identifier string idempotent bool reasons []RetryReason } func (mgr *mockRetryRequest) RetryAttempts() uint32 { return mgr.attempts } func (mgr *mockRetryRequest) IncrementRetryAttempts() { mgr.attempts++ } func (mgr *mockRetryRequest) Identifier() string { return mgr.identifier } func (mgr *mockRetryRequest) Idempotent() bool { return mgr.idempotent } func (mgr *mockRetryRequest) RetryReasons() []RetryReason { return mgr.reasons } type mockRetryStrategy struct { retried bool action RetryAction } func (mrs *mockRetryStrategy) RetryAfter(req RetryRequest, reason RetryReason) RetryAction { mrs.retried = true return mrs.action } func mockBackoffCalculator(retryAttempts uint32) time.Duration { return time.Millisecond * time.Duration(retryAttempts) } func (suite *UnitTestSuite) TestRetryWrapper_ForwardsAttempt() { expectedAction := &NoRetryRetryAction{} strategy := newRetryStrategyWrapper(&mockRetryStrategy{action: expectedAction}) request := &mockGocbcoreRequest{ reasons: []gocbcore.RetryReason{gocbcore.KVCollectionOutdatedRetryReason, gocbcore.UnknownRetryReason}, } action := strategy.RetryAfter(request, gocbcore.UnknownRetryReason) if action != expectedAction { suite.T().Fatalf("Expected retry action to be %v but was %v", expectedAction, action) } } func (suite *UnitTestSuite) TestBestEffortRetryStrategy_RetryAfterNoRetry() { strategy := NewBestEffortRetryStrategy(mockBackoffCalculator) action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.UnknownRetryReason)) if action.Duration() != 0 { suite.T().Fatalf("Expected duration to be %d but was %d", 0, action.Duration()) } } func (suite *UnitTestSuite) TestBestEffortRetryStrategy_RetryAfterAlwaysRetry() { strategy := NewBestEffortRetryStrategy(mockBackoffCalculator) action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.KVCollectionOutdatedRetryReason)) if action.Duration() != 0 { suite.T().Fatalf("Expected duration to be %d but was %d", 0, action.Duration()) } action = strategy.RetryAfter(&mockRetryRequest{attempts: 5}, RetryReason(gocbcore.KVCollectionOutdatedRetryReason)) if action.Duration() != 5*time.Millisecond { suite.T().Fatalf("Expected duration to be %d but was %d", 5*time.Millisecond, action.Duration()) } } func (suite *UnitTestSuite) TestBestEffortRetryStrategy_RetryAfterAllowsNonIdempotent() { strategy := NewBestEffortRetryStrategy(mockBackoffCalculator) action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.KVLockedRetryReason)) if action.Duration() != 0 { suite.T().Fatalf("Expected duration to be %d but was %d", 0, action.Duration()) } action = strategy.RetryAfter(&mockRetryRequest{attempts: 5}, RetryReason(gocbcore.KVLockedRetryReason)) if action.Duration() != 5*time.Millisecond { suite.T().Fatalf("Expected duration to be %d but was %d", 5*time.Millisecond, action.Duration()) } } func (suite *UnitTestSuite) TestBestEffortRetryStrategy_RetryAfterDefaultCalculator() { strategy := NewBestEffortRetryStrategy(nil) action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.KVCollectionOutdatedRetryReason)) if action.Duration() != 1*time.Millisecond { suite.T().Fatalf("Expected duration to be %d but was %d", 1*time.Millisecond, action.Duration()) } action = strategy.RetryAfter(&mockRetryRequest{attempts: 5}, RetryReason(gocbcore.KVLockedRetryReason)) if action.Duration() != 32*time.Millisecond { suite.T().Fatalf("Expected duration to be %d but was %d", 32*time.Millisecond, action.Duration()) } } func (suite *UnitTestSuite) TestFailFastRetryStrategy_RetryAfterNoRetry() { strategy := newFailFastRetryStrategy() action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.UnknownRetryReason)) if action.Duration() != 0 { suite.T().Fatalf("Expected duration to be %d but was %d", 0, action.Duration()) } } func (suite *UnitTestSuite) TestFailFastRetryStrategy_RetryAfterAlwaysRetry() { strategy := newFailFastRetryStrategy() action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.KVCollectionOutdatedRetryReason)) if action.Duration() != 0 { suite.T().Fatalf("Expected duration to be %d but was %d", 0, action.Duration()) } } func (suite *UnitTestSuite) TestFailFastRetryStrategy_RetryAfterAllowsNonIdempotent() { strategy := newFailFastRetryStrategy() action := strategy.RetryAfter(&mockRetryRequest{}, RetryReason(gocbcore.KVLockedRetryReason)) if action.Duration() != 0 { suite.T().Fatalf("Expected duration to be %d but was %d", 0, action.Duration()) } } gocb-2.6.3/scope.go000066400000000000000000000030361441755043100141020ustar00rootroot00000000000000package gocb // Scope represents a single scope within a bucket. type Scope struct { scopeName string bucket *Bucket timeoutsConfig TimeoutsConfig transcoder Transcoder retryStrategyWrapper *retryStrategyWrapper tracer RequestTracer meter *meterWrapper useMutationTokens bool getKvProvider func() (kvProvider, error) getQueryProvider func() (queryProvider, error) getAnalyticsProvider func() (analyticsProvider, error) getTransactions func() *Transactions } func newScope(bucket *Bucket, scopeName string) *Scope { return &Scope{ scopeName: scopeName, bucket: bucket, timeoutsConfig: bucket.timeoutsConfig, transcoder: bucket.transcoder, retryStrategyWrapper: bucket.retryStrategyWrapper, tracer: bucket.tracer, meter: bucket.meter, useMutationTokens: bucket.useMutationTokens, getKvProvider: bucket.getKvProvider, getQueryProvider: bucket.getQueryProvider, getAnalyticsProvider: bucket.getAnalyticsProvider, getTransactions: bucket.getTransactions, } } // Name returns the name of the scope. func (s *Scope) Name() string { return s.scopeName } // BucketName returns the name of the bucket to which this collection belongs. // UNCOMMITTED: This API may change in the future. func (s *Scope) BucketName() string { return s.bucket.Name() } // Collection returns an instance of a collection. func (s *Scope) Collection(collectionName string) *Collection { return newCollection(s, collectionName) } gocb-2.6.3/scope_analyticsquery.go000066400000000000000000000033361441755043100172420ustar00rootroot00000000000000package gocb import ( "fmt" "time" ) // AnalyticsQuery executes the analytics query statement on the server, constraining the query to the bucket and scope. func (s *Scope) AnalyticsQuery(statement string, opts *AnalyticsOptions) (*AnalyticsResult, error) { if opts == nil { opts = &AnalyticsOptions{} } start := time.Now() defer s.meter.ValueRecord(meterValueServiceAnalytics, "analytics", start) span := createSpan(s.tracer, opts.ParentSpan, "analytics", "analytics") span.SetAttribute("db.statement", statement) span.SetAttribute("db.name", s.BucketName()) span.SetAttribute("db.couchbase.scope", s.Name()) defer span.End() timeout := opts.Timeout if opts.Timeout == 0 { timeout = s.timeoutsConfig.AnalyticsTimeout } deadline := time.Now().Add(timeout) retryStrategy := s.retryStrategyWrapper if opts.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(opts.RetryStrategy) } queryOpts, err := opts.toMap() if err != nil { return nil, AnalyticsError{ InnerError: wrapError(err, "failed to generate query options"), Statement: statement, ClientContextID: opts.ClientContextID, } } var priorityInt int32 if opts.Priority { priorityInt = -1 } queryOpts["statement"] = statement queryOpts["query_context"] = fmt.Sprintf("default:`%s`.`%s`", s.BucketName(), s.Name()) provider, err := s.getAnalyticsProvider() if err != nil { return nil, AnalyticsError{ InnerError: wrapError(err, "failed to get query provider"), Statement: statement, ClientContextID: maybeGetAnalyticsOption(queryOpts, "client_context_id"), } } return execAnalyticsQuery(opts.Context, span, queryOpts, priorityInt, deadline, retryStrategy, provider, s.tracer, opts.Internal.User) } gocb-2.6.3/scope_analyticsquery_test.go000066400000000000000000000020201441755043100202660ustar00rootroot00000000000000package gocb import ( "fmt" "time" ) func (suite *IntegrationTestSuite) TestScopeAnalyticsQuery() { suite.skipIfUnsupported(CollectionsAnalyticsFeature) n := suite.setupScopeAnalytics() query := fmt.Sprintf("SELECT * FROM %s.%s.%s WHERE service=? LIMIT %d;", globalBucket.Name(), globalScope.Name(), globalCollection.Name(), n) suite.runAnalyticsTest(n, query, globalBucket.Name(), globalScope.Name(), globalScope) } func (suite *IntegrationTestSuite) setupScopeAnalytics() int { n, err := suite.createBreweryDataset("beer_sample_brewery_five", "analytics", "", globalCollection.Name()) suite.Require().Nil(err, "Failed to create dataset %v", err) results, err := globalCluster.AnalyticsQuery( fmt.Sprintf("ALTER COLLECTION %s.%s.%s ENABLE ANALYTICS", globalBucket.Name(), globalScope.Name(), globalCollection.Name(), ), &AnalyticsOptions{ Timeout: 10 * time.Second, }, ) suite.Require().Nil(err, "Failed to create analytics collection %v", err) suite.Require().Nil(results.Close()) return n } gocb-2.6.3/scope_query.go000066400000000000000000000032631441755043100153310ustar00rootroot00000000000000package gocb import ( "fmt" "time" ) // Query executes the query statement on the server, constraining the query to the bucket and scope. func (s *Scope) Query(statement string, opts *QueryOptions) (*QueryResult, error) { if opts == nil { opts = &QueryOptions{} } if opts.AsTransaction != nil { return s.getTransactions().singleQuery(statement, s, *opts) } start := time.Now() defer s.meter.ValueRecord(meterValueServiceQuery, "query", start) span := createSpan(s.tracer, opts.ParentSpan, "query", "query") span.SetAttribute("db.statement", statement) span.SetAttribute("db.name", s.BucketName()) span.SetAttribute("db.couchbase.scope", s.Name()) defer span.End() timeout := opts.Timeout if timeout == 0 { timeout = s.timeoutsConfig.QueryTimeout } deadline := time.Now().Add(timeout) retryStrategy := s.retryStrategyWrapper if opts.RetryStrategy != nil { retryStrategy = newRetryStrategyWrapper(opts.RetryStrategy) } queryOpts, err := opts.toMap() if err != nil { return nil, QueryError{ InnerError: wrapError(err, "failed to generate query options"), Statement: statement, ClientContextID: opts.ClientContextID, } } queryOpts["statement"] = statement queryOpts["query_context"] = fmt.Sprintf("%s.%s", s.BucketName(), s.Name()) provider, err := s.getQueryProvider() if err != nil { return nil, QueryError{ InnerError: wrapError(err, "failed to get query provider"), Statement: statement, ClientContextID: maybeGetQueryOption(queryOpts, "client_context_id"), } } return execN1qlQuery(opts.Context, span, queryOpts, deadline, retryStrategy, opts.Adhoc, provider, s.tracer, opts.Internal.User, opts.Internal.Endpoint) } gocb-2.6.3/scope_query_test.go000066400000000000000000000170551441755043100163740ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "fmt" "github.com/google/uuid" "time" "github.com/couchbase/gocbcore/v10" "github.com/stretchr/testify/mock" ) func (suite *IntegrationTestSuite) TestScopeQuery() { suite.skipIfUnsupported(CollectionsQueryFeature) n := suite.setupScopeQuery() suite.Run("TestScopeQuery", func() { suite.runScopeQueryPositionalTest(n, true) suite.runScopeQueryNamedTest(n, true) }) suite.Run("TestScopeQueryNoMetrics", func() { suite.runScopeQueryPositionalTest(n, false) suite.runScopeQueryNamedTest(n, false) }) suite.Run("TestScopePreparedQuery", func() { suite.runScopePreparedQueryPositionalTest(n) suite.runScopePreparedQueryNamedTest(n) }) } func (suite *IntegrationTestSuite) setupScopeQuery() int { n, err := suite.createBreweryDataset("beer_sample_brewery_five", "scopequery", globalScope.Name(), globalCollection.Name()) suite.Require().Nil(err, "Failed to create dataset %v", err) _, err = globalScope.Query(fmt.Sprintf("CREATE PRIMARY INDEX ON `%s`", globalCollection.Name()), nil) if err != nil { mgr := globalCluster.QueryIndexes() err = mgr.base.tryParseErrorMessage(err) if !errors.Is(err, ErrIndexExists) { suite.T().Fatalf("Failed to create index %v", err) } } return n } func (suite *IntegrationTestSuite) runScopePreparedQueryPositionalTest(n int) { query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=? LIMIT %d;", globalCollection.Name(), globalCollection.Name(), n) suite.runPreparedQueryTest(n, query, globalBucket.Name(), globalScope.Name(), globalScope, []interface{}{"scopequery"}) } func (suite *IntegrationTestSuite) runScopePreparedQueryNamedTest(n int) { query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=$service LIMIT %d;", globalCollection.Name(), globalCollection.Name(), n) suite.runPreparedQueryTest(n, query, globalBucket.Name(), globalScope.Name(), globalScope, map[string]interface{}{"service": "scopequery"}) } func (suite *IntegrationTestSuite) runScopeQueryPositionalTest(n int, withMetrics bool) { query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=? LIMIT %d;", globalCollection.Name(), globalCollection.Name(), n) suite.runQueryTest(n, query, globalBucket.Name(), globalScope.Name(), globalScope, withMetrics, []interface{}{"scopequery"}) } func (suite *IntegrationTestSuite) runScopeQueryNamedTest(n int, withMetrics bool) { query := fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE service=$service LIMIT %d;", globalCollection.Name(), globalCollection.Name(), n) suite.runQueryTest(n, query, globalBucket.Name(), globalScope.Name(), globalScope, withMetrics, map[string]interface{}{"service": "scopequery"}) } func (suite *UnitTestSuite) queryScope(prepared bool, reader queryRowReader, runFn func(args mock.Arguments)) *Scope { queryProvider, call := suite.newMockQueryProvider(prepared, reader) call.Run(runFn) cli := new(mockConnectionManager) cli.On("getQueryProvider").Return(queryProvider, nil) b := suite.bucket("queryBucket", TimeoutsConfig{QueryTimeout: 75 * time.Second}, cli) scope := suite.newScope(b, "queryScope") return scope } func (suite *UnitTestSuite) TestScopeQueryPrepared() { var dataset testQueryDataset err := loadJSONTestDataset("beer_sample_query_dataset", &dataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: dataset.Results, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, PName: dataset.jsonQueryResponse.Prepared, }, } statement := "SELECT * FROM dataset" var scope *Scope scope = suite.queryScope(true, reader, func(args mock.Arguments) { opts := args.Get(1).(gocbcore.N1QLQueryOptions) suite.Assert().Equal(scope.retryStrategyWrapper, opts.RetryStrategy) now := time.Now() if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) { suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline) } var actualOptions map[string]interface{} err := json.Unmarshal(opts.Payload, &actualOptions) suite.Require().Nil(err) suite.Assert().Contains(actualOptions, "client_context_id") suite.Assert().Equal(actualOptions["query_context"], "queryBucket.queryScope") }) result, err := scope.Query(statement, nil) suite.Require().Nil(err, err) suite.Require().NotNil(result) suite.assertQueryBeerResult(dataset, result) } func (suite *IntegrationTestSuite) TestScopeQueryTransaction() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(TransactionsFeature) mgr := globalCollection.QueryIndexes() err := mgr.CreatePrimaryIndex(&CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) // Ensure the index is online suite.Eventually(func() bool { res, err := globalScope.Query(fmt.Sprintf("SELECT 1 FROM %s", globalCollection.Name()), &QueryOptions{ Adhoc: true, }) if err != nil { return false } for res.Next() { } err = res.Err() return err == nil }, 30*time.Second, 500*time.Millisecond) docID := uuid.New().String() res, err := globalScope.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {})", globalCollection.Name(), docID), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) suite.Require().Nil(err, err) for res.Next() { } err = res.Err() suite.Require().Nil(err, err) meta, err := res.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(uint64(1), meta.Metrics.MutationCount) // Verify that we've inserted into the correct place. getRes, err := globalCollection.Get(docID, &GetOptions{ Transcoder: NewRawJSONTranscoder(), }) suite.Require().Nil(err, err) var getResBytes []byte err = getRes.Content(&getResBytes) suite.Require().Nil(err, err) suite.Assert().Equal([]byte("{}"), getResBytes) } func (suite *IntegrationTestSuite) TestScopeQueryTransactionDoubleInsert() { suite.skipIfUnsupported(QueryFeature) suite.skipIfUnsupported(TransactionsFeature) mgr := globalCollection.QueryIndexes() err := mgr.CreatePrimaryIndex(&CreatePrimaryQueryIndexOptions{ IgnoreIfExists: true, }) suite.Require().Nil(err, err) suite.Eventually(func() bool { res, err := globalScope.Query(fmt.Sprintf("SELECT 1 FROM %s", globalCollection.Name()), &QueryOptions{ Adhoc: true, }) if err != nil { return false } for res.Next() { } err = res.Err() suite.Require().Nil(err, err) return err == nil }, 30*time.Second, 500*time.Millisecond) docID := uuid.New().String() res, err := globalScope.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {})", globalCollection.Name(), docID), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) suite.Require().Nil(err, err) for res.Next() { } err = res.Err() suite.Require().Nil(err, err) meta, err := res.MetaData() suite.Require().Nil(err, err) suite.Assert().Equal(uint64(1), meta.Metrics.MutationCount) _, err = globalScope.Query(fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {})", globalCollection.Name(), docID), &QueryOptions{ AsTransaction: &SingleQueryTransactionOptions{ DurabilityLevel: DurabilityLevelMajority, }, Adhoc: true, }) if globalCluster.SupportsFeature(TransactionsSingleQueryExistsErrorFeature) { var tErr *TransactionFailedError if errors.As(err, &tErr) { suite.T().Logf("Error should have not have been TransactionFailed but was: %v", err) suite.T().Fail() } suite.Require().ErrorIs(err, ErrDocumentExists) } else { var tErr *TransactionFailedError suite.Assert().ErrorAs(err, &tErr) } } gocb-2.6.3/search/000077500000000000000000000000001441755043100137055ustar00rootroot00000000000000gocb-2.6.3/search/facets.go000066400000000000000000000053141441755043100155040ustar00rootroot00000000000000package search import ( "encoding/json" ) // Facet represents a facet for a search query. type Facet interface { } type termFacetData struct { Field string `json:"field,omitempty"` Size uint64 `json:"size,omitempty"` } // TermFacet is an search term facet. type TermFacet struct { data termFacetData } // MarshalJSON marshal's this facet to JSON for the search REST API. func (f TermFacet) MarshalJSON() ([]byte, error) { return json.Marshal(f.data) } // NewTermFacet creates a new TermFacet func NewTermFacet(field string, size uint64) *TermFacet { mq := &TermFacet{} mq.data.Field = field mq.data.Size = size return mq } type numericFacetRange struct { Name string `json:"name,omitempty"` Start float64 `json:"min,omitempty"` End float64 `json:"max,omitempty"` } type numericFacetData struct { Field string `json:"field,omitempty"` Size uint64 `json:"size,omitempty"` NumericRanges []numericFacetRange `json:"numeric_ranges,omitempty"` } // NumericFacet is an search numeric range facet. type NumericFacet struct { data numericFacetData } // MarshalJSON marshal's this facet to JSON for the search REST API. func (f NumericFacet) MarshalJSON() ([]byte, error) { return json.Marshal(f.data) } // AddRange adds a new range to this numeric range facet. func (f *NumericFacet) AddRange(name string, start, end float64) *NumericFacet { f.data.NumericRanges = append(f.data.NumericRanges, numericFacetRange{ Name: name, Start: start, End: end, }) return f } // NewNumericFacet creates a new numeric range facet. func NewNumericFacet(field string, size uint64) *NumericFacet { mq := &NumericFacet{} mq.data.Field = field mq.data.Size = size return mq } type dateFacetRange struct { Name string `json:"name,omitempty"` Start string `json:"start,omitempty"` End string `json:"end,omitempty"` } type dateFacetData struct { Field string `json:"field,omitempty"` Size uint64 `json:"size,omitempty"` DateRanges []dateFacetRange `json:"date_ranges,omitempty"` } // DateFacet is an search date range facet. type DateFacet struct { data dateFacetData } // MarshalJSON marshal's this facet to JSON for the search REST API. func (f DateFacet) MarshalJSON() ([]byte, error) { return json.Marshal(f.data) } // AddRange adds a new range to this date range facet. func (f *DateFacet) AddRange(name string, start, end string) *DateFacet { f.data.DateRanges = append(f.data.DateRanges, dateFacetRange{ Name: name, Start: start, End: end, }) return f } // NewDateFacet creates a new date range facet. func NewDateFacet(field string, size uint64) *DateFacet { mq := &DateFacet{} mq.data.Field = field mq.data.Size = size return mq } gocb-2.6.3/search/queries.go000066400000000000000000000426111441755043100157150ustar00rootroot00000000000000package search import "encoding/json" // Query represents a search query. type Query interface { } type searchQueryBase struct { options map[string]interface{} } func newSearchQueryBase() searchQueryBase { return searchQueryBase{ options: make(map[string]interface{}), } } // MarshalJSON marshal's this query to JSON for the search REST API. func (q searchQueryBase) MarshalJSON() ([]byte, error) { return json.Marshal(q.options) } // MatchOperator defines how the individual match terms should be logically concatenated. type MatchOperator string const ( // MatchOperatorOr specifies that individual match terms are concatenated with a logical OR - this is the default if not provided. MatchOperatorOr MatchOperator = "or" // MatchOperatorAnd specifies that individual match terms are concatenated with a logical AND. MatchOperatorAnd MatchOperator = "and" ) // MatchQuery represents a search match query. type MatchQuery struct { searchQueryBase } // NewMatchQuery creates a new MatchQuery. func NewMatchQuery(match string) *MatchQuery { q := &MatchQuery{newSearchQueryBase()} q.options["match"] = match return q } // Field specifies the field for this query. func (q *MatchQuery) Field(field string) *MatchQuery { q.options["field"] = field return q } // Analyzer specifies the analyzer to use for this query. func (q *MatchQuery) Analyzer(analyzer string) *MatchQuery { q.options["analyzer"] = analyzer return q } // PrefixLength specifies the prefix length from this query. func (q *MatchQuery) PrefixLength(length uint64) *MatchQuery { q.options["prefix_length"] = length return q } // Fuzziness specifies the fuziness for this query. func (q *MatchQuery) Fuzziness(fuzziness uint64) *MatchQuery { q.options["fuzziness"] = fuzziness return q } // Boost specifies the boost for this query. func (q *MatchQuery) Boost(boost float32) *MatchQuery { q.options["boost"] = boost return q } // Operator defines how the individual match terms should be logically concatenated. func (q *MatchQuery) Operator(operator MatchOperator) *MatchQuery { q.options["operator"] = string(operator) return q } // MatchPhraseQuery represents a search match phrase query. type MatchPhraseQuery struct { searchQueryBase } // NewMatchPhraseQuery creates a new MatchPhraseQuery func NewMatchPhraseQuery(phrase string) *MatchPhraseQuery { q := &MatchPhraseQuery{newSearchQueryBase()} q.options["match_phrase"] = phrase return q } // Field specifies the field for this query. func (q *MatchPhraseQuery) Field(field string) *MatchPhraseQuery { q.options["field"] = field return q } // Analyzer specifies the analyzer to use for this query. func (q *MatchPhraseQuery) Analyzer(analyzer string) *MatchPhraseQuery { q.options["analyzer"] = analyzer return q } // Boost specifies the boost for this query. func (q *MatchPhraseQuery) Boost(boost float32) *MatchPhraseQuery { q.options["boost"] = boost return q } // RegexpQuery represents a search regular expression query. type RegexpQuery struct { searchQueryBase } // NewRegexpQuery creates a new RegexpQuery. func NewRegexpQuery(regexp string) *RegexpQuery { q := &RegexpQuery{newSearchQueryBase()} q.options["regexp"] = regexp return q } // Field specifies the field for this query. func (q *RegexpQuery) Field(field string) *RegexpQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *RegexpQuery) Boost(boost float32) *RegexpQuery { q.options["boost"] = boost return q } // QueryStringQuery represents a search string query. type QueryStringQuery struct { searchQueryBase } // NewQueryStringQuery creates a new StringQuery. func NewQueryStringQuery(query string) *QueryStringQuery { q := &QueryStringQuery{newSearchQueryBase()} q.options["query"] = query return q } // Boost specifies the boost for this query. func (q *QueryStringQuery) Boost(boost float32) *QueryStringQuery { q.options["boost"] = boost return q } // NumericRangeQuery represents a search numeric range query. type NumericRangeQuery struct { searchQueryBase } // NewNumericRangeQuery creates a new NumericRangeQuery. func NewNumericRangeQuery() *NumericRangeQuery { q := &NumericRangeQuery{newSearchQueryBase()} return q } // Min specifies the minimum value and inclusiveness for this range query. func (q *NumericRangeQuery) Min(min float32, inclusive bool) *NumericRangeQuery { q.options["min"] = min q.options["inclusive_min"] = inclusive return q } // Max specifies the maximum value and inclusiveness for this range query. func (q *NumericRangeQuery) Max(max float32, inclusive bool) *NumericRangeQuery { q.options["max"] = max q.options["inclusive_max"] = inclusive return q } // Field specifies the field for this query. func (q *NumericRangeQuery) Field(field string) *NumericRangeQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *NumericRangeQuery) Boost(boost float32) *NumericRangeQuery { q.options["boost"] = boost return q } // DateRangeQuery represents a search date range query. type DateRangeQuery struct { searchQueryBase } // NewDateRangeQuery creates a new DateRangeQuery. func NewDateRangeQuery() *DateRangeQuery { q := &DateRangeQuery{newSearchQueryBase()} return q } // Start specifies the start value and inclusiveness for this range query. func (q *DateRangeQuery) Start(start string, inclusive bool) *DateRangeQuery { q.options["start"] = start q.options["inclusive_start"] = inclusive return q } // End specifies the end value and inclusiveness for this range query. func (q *DateRangeQuery) End(end string, inclusive bool) *DateRangeQuery { q.options["end"] = end q.options["inclusive_end"] = inclusive return q } // DateTimeParser specifies which date time string parser to use. func (q *DateRangeQuery) DateTimeParser(parser string) *DateRangeQuery { q.options["datetime_parser"] = parser return q } // Field specifies the field for this query. func (q *DateRangeQuery) Field(field string) *DateRangeQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *DateRangeQuery) Boost(boost float32) *DateRangeQuery { q.options["boost"] = boost return q } // ConjunctionQuery represents a search conjunction query. type ConjunctionQuery struct { searchQueryBase } // NewConjunctionQuery creates a new ConjunctionQuery. func NewConjunctionQuery(queries ...Query) *ConjunctionQuery { q := &ConjunctionQuery{newSearchQueryBase()} q.options["conjuncts"] = []Query{} return q.And(queries...) } // And adds new predicate queries to this conjunction query. func (q *ConjunctionQuery) And(queries ...Query) *ConjunctionQuery { q.options["conjuncts"] = append(q.options["conjuncts"].([]Query), queries...) return q } // Boost specifies the boost for this query. func (q *ConjunctionQuery) Boost(boost float32) *ConjunctionQuery { q.options["boost"] = boost return q } // DisjunctionQuery represents a search disjunction query. type DisjunctionQuery struct { searchQueryBase } // NewDisjunctionQuery creates a new DisjunctionQuery. func NewDisjunctionQuery(queries ...Query) *DisjunctionQuery { q := &DisjunctionQuery{newSearchQueryBase()} q.options["disjuncts"] = []Query{} return q.Or(queries...) } // Or adds new predicate queries to this disjunction query. func (q *DisjunctionQuery) Or(queries ...Query) *DisjunctionQuery { q.options["disjuncts"] = append(q.options["disjuncts"].([]Query), queries...) return q } // Boost specifies the boost for this query. func (q *DisjunctionQuery) Boost(boost float32) *DisjunctionQuery { q.options["boost"] = boost return q } // Min specifies the minimum number of queries that a document must satisfy. func (q *DisjunctionQuery) Min(min uint32) *DisjunctionQuery { q.options["min"] = min return q } type booleanQueryData struct { Must *ConjunctionQuery `json:"must,omitempty"` Should *DisjunctionQuery `json:"should,omitempty"` MustNot *DisjunctionQuery `json:"must_not,omitempty"` Boost float32 `json:"boost,omitempty"` } // BooleanQuery represents a search boolean query. type BooleanQuery struct { data booleanQueryData shouldMin uint32 } // NewBooleanQuery creates a new BooleanQuery. func NewBooleanQuery() *BooleanQuery { q := &BooleanQuery{} return q } // Must specifies a query which must match. func (q *BooleanQuery) Must(query Query) *BooleanQuery { switch val := query.(type) { case ConjunctionQuery: q.data.Must = &val case *ConjunctionQuery: q.data.Must = val default: q.data.Must = NewConjunctionQuery(val) } return q } // Should specifies a query which should match. func (q *BooleanQuery) Should(query Query) *BooleanQuery { switch val := query.(type) { case DisjunctionQuery: q.data.Should = &val case *DisjunctionQuery: q.data.Should = val default: q.data.Should = NewDisjunctionQuery(val) } return q } // MustNot specifies a query which must not match. func (q *BooleanQuery) MustNot(query Query) *BooleanQuery { switch val := query.(type) { case DisjunctionQuery: q.data.MustNot = &val case *DisjunctionQuery: q.data.MustNot = val default: q.data.MustNot = NewDisjunctionQuery(val) } return q } // ShouldMin specifies the minimum value before the should query will boost. func (q *BooleanQuery) ShouldMin(min uint32) *BooleanQuery { q.shouldMin = min return q } // Boost specifies the boost for this query. func (q *BooleanQuery) Boost(boost float32) *BooleanQuery { q.data.Boost = boost return q } // MarshalJSON marshal's this query to JSON for the search REST API. func (q *BooleanQuery) MarshalJSON() ([]byte, error) { if q.data.Should != nil { q.data.Should.options["min"] = q.shouldMin } bytes, err := json.Marshal(q.data) if q.data.Should != nil { delete(q.data.Should.options, "min") } return bytes, err } // WildcardQuery represents a search wildcard query. type WildcardQuery struct { searchQueryBase } // NewWildcardQuery creates a new WildcardQuery. func NewWildcardQuery(wildcard string) *WildcardQuery { q := &WildcardQuery{newSearchQueryBase()} q.options["wildcard"] = wildcard return q } // Field specifies the field for this query. func (q *WildcardQuery) Field(field string) *WildcardQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *WildcardQuery) Boost(boost float32) *WildcardQuery { q.options["boost"] = boost return q } // DocIDQuery represents a search document id query. type DocIDQuery struct { searchQueryBase } // NewDocIDQuery creates a new DocIdQuery. func NewDocIDQuery(ids ...string) *DocIDQuery { q := &DocIDQuery{newSearchQueryBase()} q.options["ids"] = []string{} return q.AddDocIds(ids...) } // AddDocIds adds addition document ids to this query. func (q *DocIDQuery) AddDocIds(ids ...string) *DocIDQuery { q.options["ids"] = append(q.options["ids"].([]string), ids...) return q } // Field specifies the field for this query. func (q *DocIDQuery) Field(field string) *DocIDQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *DocIDQuery) Boost(boost float32) *DocIDQuery { q.options["boost"] = boost return q } // BooleanFieldQuery represents a search boolean field query. type BooleanFieldQuery struct { searchQueryBase } // NewBooleanFieldQuery creates a new BooleanFieldQuery. func NewBooleanFieldQuery(val bool) *BooleanFieldQuery { q := &BooleanFieldQuery{newSearchQueryBase()} q.options["bool"] = val return q } // Field specifies the field for this query. func (q *BooleanFieldQuery) Field(field string) *BooleanFieldQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *BooleanFieldQuery) Boost(boost float32) *BooleanFieldQuery { q.options["boost"] = boost return q } // TermQuery represents a search term query. type TermQuery struct { searchQueryBase } // NewTermQuery creates a new TermQuery. func NewTermQuery(term string) *TermQuery { q := &TermQuery{newSearchQueryBase()} q.options["term"] = term return q } // Field specifies the field for this query. func (q *TermQuery) Field(field string) *TermQuery { q.options["field"] = field return q } // PrefixLength specifies the prefix length from this query. func (q *TermQuery) PrefixLength(length uint64) *TermQuery { q.options["prefix_length"] = length return q } // Fuzziness specifies the fuziness for this query. func (q *TermQuery) Fuzziness(fuzziness uint64) *TermQuery { q.options["fuzziness"] = fuzziness return q } // Boost specifies the boost for this query. func (q *TermQuery) Boost(boost float32) *TermQuery { q.options["boost"] = boost return q } // PhraseQuery represents a search phrase query. type PhraseQuery struct { searchQueryBase } // NewPhraseQuery creates a new PhraseQuery. func NewPhraseQuery(terms ...string) *PhraseQuery { q := &PhraseQuery{newSearchQueryBase()} q.options["terms"] = terms return q } // Field specifies the field for this query. func (q *PhraseQuery) Field(field string) *PhraseQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *PhraseQuery) Boost(boost float32) *PhraseQuery { q.options["boost"] = boost return q } // PrefixQuery represents a search prefix query. type PrefixQuery struct { searchQueryBase } // NewPrefixQuery creates a new PrefixQuery. func NewPrefixQuery(prefix string) *PrefixQuery { q := &PrefixQuery{newSearchQueryBase()} q.options["prefix"] = prefix return q } // Field specifies the field for this query. func (q *PrefixQuery) Field(field string) *PrefixQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *PrefixQuery) Boost(boost float32) *PrefixQuery { q.options["boost"] = boost return q } // MatchAllQuery represents a search match all query. type MatchAllQuery struct { searchQueryBase } // NewMatchAllQuery creates a new MatchAllQuery. func NewMatchAllQuery() *MatchAllQuery { q := &MatchAllQuery{newSearchQueryBase()} q.options["match_all"] = nil return q } // MatchNoneQuery represents a search match none query. type MatchNoneQuery struct { searchQueryBase } // NewMatchNoneQuery creates a new MatchNoneQuery. func NewMatchNoneQuery() *MatchNoneQuery { q := &MatchNoneQuery{newSearchQueryBase()} q.options["match_none"] = nil return q } // TermRangeQuery represents a search term range query. type TermRangeQuery struct { searchQueryBase } // NewTermRangeQuery creates a new TermRangeQuery. func NewTermRangeQuery(term string) *TermRangeQuery { q := &TermRangeQuery{newSearchQueryBase()} q.options["term"] = term return q } // Field specifies the field for this query. func (q *TermRangeQuery) Field(field string) *TermRangeQuery { q.options["field"] = field return q } // Min specifies the minimum value and inclusiveness for this range query. func (q *TermRangeQuery) Min(min string, inclusive bool) *TermRangeQuery { q.options["min"] = min q.options["inclusive_min"] = inclusive return q } // Max specifies the maximum value and inclusiveness for this range query. func (q *TermRangeQuery) Max(max string, inclusive bool) *TermRangeQuery { q.options["max"] = max q.options["inclusive_max"] = inclusive return q } // Boost specifies the boost for this query. func (q *TermRangeQuery) Boost(boost float32) *TermRangeQuery { q.options["boost"] = boost return q } // GeoDistanceQuery represents a search geographical distance query. type GeoDistanceQuery struct { searchQueryBase } // NewGeoDistanceQuery creates a new GeoDistanceQuery. func NewGeoDistanceQuery(lon, lat float64, distance string) *GeoDistanceQuery { q := &GeoDistanceQuery{newSearchQueryBase()} q.options["location"] = []float64{lon, lat} q.options["distance"] = distance return q } // Field specifies the field for this query. func (q *GeoDistanceQuery) Field(field string) *GeoDistanceQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *GeoDistanceQuery) Boost(boost float32) *GeoDistanceQuery { q.options["boost"] = boost return q } // GeoBoundingBoxQuery represents a search geographical bounding box query. type GeoBoundingBoxQuery struct { searchQueryBase } // NewGeoBoundingBoxQuery creates a new GeoBoundingBoxQuery. func NewGeoBoundingBoxQuery(tlLon, tlLat, brLon, brLat float64) *GeoBoundingBoxQuery { q := &GeoBoundingBoxQuery{newSearchQueryBase()} q.options["top_left"] = []float64{tlLon, tlLat} q.options["bottom_right"] = []float64{brLon, brLat} return q } // Field specifies the field for this query. func (q *GeoBoundingBoxQuery) Field(field string) *GeoBoundingBoxQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *GeoBoundingBoxQuery) Boost(boost float32) *GeoBoundingBoxQuery { q.options["boost"] = boost return q } // Coordinate is a tuple of a latitude and a longitude. type Coordinate struct { Lon float64 Lat float64 } // GeoPolygonQuery represents a search query which allows to match inside a geo polygon. type GeoPolygonQuery struct { searchQueryBase } // NewGeoPolygonQuery creates a new GeoPolygonQuery. func NewGeoPolygonQuery(coords []Coordinate) *GeoPolygonQuery { q := &GeoPolygonQuery{newSearchQueryBase()} var polyPoints [][]float64 for _, coord := range coords { polyPoints = append(polyPoints, []float64{coord.Lon, coord.Lat}) } q.options["polygon_points"] = polyPoints return q } // Field specifies the field for this query. func (q *GeoPolygonQuery) Field(field string) *GeoPolygonQuery { q.options["field"] = field return q } // Boost specifies the boost for this query. func (q *GeoPolygonQuery) Boost(boost float32) *GeoPolygonQuery { q.options["boost"] = boost return q } gocb-2.6.3/search/sorting.go000066400000000000000000000060721441755043100157260ustar00rootroot00000000000000package search import ( "encoding/json" ) // SearchSort represents an search sorting for a search query. type Sort interface { } type searchSortBase struct { options map[string]interface{} } func newSearchSortBase() searchSortBase { return searchSortBase{ options: make(map[string]interface{}), } } // MarshalJSON marshal's this query to JSON for the search REST API. func (q searchSortBase) MarshalJSON() ([]byte, error) { return json.Marshal(q.options) } // SearchSortScore represents a search score sort. type SearchSortScore struct { searchSortBase } // NewSearchSortScore creates a new SearchSortScore. func NewSearchSortScore() *SearchSortScore { q := &SearchSortScore{newSearchSortBase()} q.options["by"] = "score" return q } // Descending specifies the ordering of the results. func (q *SearchSortScore) Descending(descending bool) *SearchSortScore { q.options["desc"] = descending return q } // SearchSortID represents a search Document ID sort. type SearchSortID struct { searchSortBase } // NewSearchSortID creates a new SearchSortScore. func NewSearchSortID() *SearchSortID { q := &SearchSortID{newSearchSortBase()} q.options["by"] = "id" return q } // Descending specifies the ordering of the results. func (q *SearchSortID) Descending(descending bool) *SearchSortID { q.options["desc"] = descending return q } // SearchSortField represents a search field sort. type SearchSortField struct { searchSortBase } // NewSearchSortField creates a new SearchSortField. func NewSearchSortField(field string) *SearchSortField { q := &SearchSortField{newSearchSortBase()} q.options["by"] = "field" q.options["field"] = field return q } // Type allows you to specify the search field sort type. func (q *SearchSortField) Type(value string) *SearchSortField { q.options["type"] = value return q } // Mode allows you to specify the search field sort mode. func (q *SearchSortField) Mode(mode string) *SearchSortField { q.options["mode"] = mode return q } // Missing allows you to specify the search field sort missing behaviour. func (q *SearchSortField) Missing(missing string) *SearchSortField { q.options["missing"] = missing return q } // Descending specifies the ordering of the results. func (q *SearchSortField) Descending(descending bool) *SearchSortField { q.options["desc"] = descending return q } // SearchSortGeoDistance represents a search geo sort. type SearchSortGeoDistance struct { searchSortBase } // NewSearchSortGeoDistance creates a new SearchSortGeoDistance. func NewSearchSortGeoDistance(field string, lon, lat float64) *SearchSortGeoDistance { q := &SearchSortGeoDistance{newSearchSortBase()} q.options["by"] = "geo_distance" q.options["field"] = field q.options["location"] = []float64{lon, lat} return q } // Unit specifies the unit used for sorting func (q *SearchSortGeoDistance) Unit(unit string) *SearchSortGeoDistance { q.options["unit"] = unit return q } // Descending specifies the ordering of the results. func (q *SearchSortGeoDistance) Descending(descending bool) *SearchSortGeoDistance { q.options["desc"] = descending return q } gocb-2.6.3/searchquery_options.go000066400000000000000000000077441441755043100171110ustar00rootroot00000000000000package gocb import ( "context" "time" cbsearch "github.com/couchbase/gocb/v2/search" ) // SearchHighlightStyle indicates the type of highlighting to use for a search query. type SearchHighlightStyle string const ( // DefaultHighlightStyle specifies to use the default to highlight search result hits. DefaultHighlightStyle SearchHighlightStyle = "" // HTMLHighlightStyle specifies to use HTML tags to highlight search result hits. HTMLHighlightStyle SearchHighlightStyle = "html" // AnsiHightlightStyle specifies to use ANSI tags to highlight search result hits. AnsiHightlightStyle SearchHighlightStyle = "ansi" ) // SearchScanConsistency indicates the level of data consistency desired for a search query. type SearchScanConsistency uint const ( searchScanConsistencyNotSet SearchScanConsistency = iota // SearchScanConsistencyNotBounded indicates no data consistency is required. SearchScanConsistencyNotBounded ) // SearchHighlightOptions are the options available for search highlighting. type SearchHighlightOptions struct { Style SearchHighlightStyle Fields []string } // SearchOptions represents a pending search query. type SearchOptions struct { ScanConsistency SearchScanConsistency Limit uint32 Skip uint32 Explain bool Highlight *SearchHighlightOptions Fields []string Sort []cbsearch.Sort Facets map[string]cbsearch.Facet ConsistentWith *MutationState // Raw provides a way to provide extra parameters in the request body for the query. Raw map[string]interface{} Timeout time.Duration RetryStrategy RetryStrategy DisableScoring bool Collections []string ParentSpan RequestSpan // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // If set to true, will include the SearchRowLocations. IncludeLocations bool // Internal: This should never be used and is not supported. Internal struct { User string } } func (opts *SearchOptions) toMap(indexName string) (map[string]interface{}, error) { data := make(map[string]interface{}) if opts.Limit > 0 { data["size"] = opts.Limit } if opts.Skip > 0 { data["from"] = opts.Skip } if opts.Explain { data["explain"] = opts.Explain } if len(opts.Fields) > 0 { data["fields"] = opts.Fields } if len(opts.Sort) > 0 { data["sort"] = opts.Sort } if opts.Highlight != nil { highlight := make(map[string]interface{}) highlight["style"] = string(opts.Highlight.Style) highlight["fields"] = opts.Highlight.Fields data["highlight"] = highlight } if opts.Facets != nil { facets := make(map[string]interface{}) for k, v := range opts.Facets { facets[k] = v } data["facets"] = facets } if opts.ScanConsistency != 0 && opts.ConsistentWith != nil { return nil, makeInvalidArgumentsError("ScanConsistency and ConsistentWith must be used exclusively") } var ctl map[string]interface{} if opts.ScanConsistency != searchScanConsistencyNotSet { consistency := make(map[string]interface{}) if opts.ScanConsistency == SearchScanConsistencyNotBounded { consistency["level"] = "" } else { return nil, makeInvalidArgumentsError("unexpected consistency option") } ctl = map[string]interface{}{"consistency": consistency} } if opts.ConsistentWith != nil { consistency := make(map[string]interface{}) consistency["level"] = "at_plus" consistency["vectors"] = opts.ConsistentWith.toSearchMutationState(indexName) if ctl == nil { ctl = make(map[string]interface{}) } ctl["consistency"] = consistency } if ctl != nil { data["ctl"] = ctl } if opts.DisableScoring { data["score"] = "none" } if opts.Raw != nil { for k, v := range opts.Raw { data[k] = v } } if len(opts.Collections) > 0 { data["collections"] = opts.Collections } if opts.IncludeLocations { data["includeLocations"] = true } return data, nil } gocb-2.6.3/singlequerytransactionresult.go000066400000000000000000000015121441755043100210420ustar00rootroot00000000000000package gocb // // SingleQueryTransactionResult allows access to the results of a single query transaction. // type SingleQueryTransactionResult struct { // wrapped *TransactionQueryResult // unstagingComplete bool // } // // // TransactionQueryResult returns the result of the query. // func (qr *SingleQueryTransactionResult) QueryResult() *TransactionQueryResult { // return qr.wrapped // } // // // UnstagingComplete returns whether all documents were successfully unstaged (committed). // // // // This will only return true if the transaction reached the COMMIT point and then went on to reach // // the COMPLETE point. // // // // It will be false for transactions that: // // - Rolled back // // - Were read-only // func (qr *SingleQueryTransactionResult) UnstagingComplete() bool { // return qr.unstagingComplete // } gocb-2.6.3/subdocspecs.go000066400000000000000000000216761441755043100153200ustar00rootroot00000000000000package gocb import "github.com/couchbase/gocbcore/v10/memd" // LookupInSpec is the representation of an operation available when calling LookupIn type LookupInSpec struct { op memd.SubDocOpType path string isXattr bool } // MutateInSpec is the representation of an operation available when calling MutateIn type MutateInSpec struct { op memd.SubDocOpType createPath bool isXattr bool path string value interface{} multiValue bool } // GetSpecOptions are the options available to LookupIn subdoc Get operations. type GetSpecOptions struct { IsXattr bool } // GetSpec indicates a path to be retrieved from the document. The value of the path // can later be retrieved from the LookupResult. // The path syntax follows query's path syntax (e.g. `foo.bar.baz`). func GetSpec(path string, opts *GetSpecOptions) LookupInSpec { if opts == nil { opts = &GetSpecOptions{} } return LookupInSpec{ op: memd.SubDocOpGet, path: path, isXattr: opts.IsXattr, } } // ExistsSpecOptions are the options available to LookupIn subdoc Exists operations. type ExistsSpecOptions struct { IsXattr bool } // ExistsSpec is similar to Path(), but does not actually retrieve the value from the server. // This may save bandwidth if you only need to check for the existence of a // path (without caring for its content). You can check the status of this // operation by using .ContentAt (and ignoring the value) or .Exists() on the LookupResult. func ExistsSpec(path string, opts *ExistsSpecOptions) LookupInSpec { if opts == nil { opts = &ExistsSpecOptions{} } return LookupInSpec{ op: memd.SubDocOpExists, path: path, isXattr: opts.IsXattr, } } // CountSpecOptions are the options available to LookupIn subdoc Count operations. type CountSpecOptions struct { IsXattr bool } // CountSpec allows you to retrieve the number of items in an array or keys within an // dictionary within an element of a document. func CountSpec(path string, opts *CountSpecOptions) LookupInSpec { if opts == nil { opts = &CountSpecOptions{} } return LookupInSpec{ op: memd.SubDocOpGetCount, path: path, isXattr: opts.IsXattr, } } // InsertSpecOptions are the options available to subdocument Insert operations. type InsertSpecOptions struct { CreatePath bool IsXattr bool } // InsertSpec inserts a value at the specified path within the document. func InsertSpec(path string, val interface{}, opts *InsertSpecOptions) MutateInSpec { if opts == nil { opts = &InsertSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpDictAdd, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: val, multiValue: false, } } // UpsertSpecOptions are the options available to subdocument Upsert operations. type UpsertSpecOptions struct { CreatePath bool IsXattr bool } // UpsertSpec creates a new value at the specified path within the document if it does not exist, if it does exist then it // updates it. func UpsertSpec(path string, val interface{}, opts *UpsertSpecOptions) MutateInSpec { if opts == nil { opts = &UpsertSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpDictSet, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: val, multiValue: false, } } // ReplaceSpecOptions are the options available to subdocument Replace operations. type ReplaceSpecOptions struct { IsXattr bool } // ReplaceSpec replaces the value of the field at path. func ReplaceSpec(path string, val interface{}, opts *ReplaceSpecOptions) MutateInSpec { if opts == nil { opts = &ReplaceSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpReplace, createPath: false, isXattr: opts.IsXattr, path: path, value: val, multiValue: false, } } // RemoveSpecOptions are the options available to subdocument Remove operations. type RemoveSpecOptions struct { IsXattr bool } // RemoveSpec removes the field at path. func RemoveSpec(path string, opts *RemoveSpecOptions) MutateInSpec { if opts == nil { opts = &RemoveSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpDelete, createPath: false, isXattr: opts.IsXattr, path: path, value: nil, multiValue: false, } } // ArrayAppendSpecOptions are the options available to subdocument ArrayAppend operations. type ArrayAppendSpecOptions struct { CreatePath bool IsXattr bool // HasMultiple adds multiple values as elements to an array. // When used `value` in the spec must be an array type // ArrayAppend("path", []int{1,2,3,4}, ArrayAppendSpecOptions{HasMultiple:true}) => // "path" [..., 1,2,3,4] // // This is a more efficient version (at both the network and server levels) // of doing // spec.ArrayAppend("path", 1, nil) // spec.ArrayAppend("path", 2, nil) // spec.ArrayAppend("path", 3, nil) HasMultiple bool } // ArrayAppendSpec adds an element(s) to the end (i.e. right) of an array func ArrayAppendSpec(path string, val interface{}, opts *ArrayAppendSpecOptions) MutateInSpec { if opts == nil { opts = &ArrayAppendSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpArrayPushLast, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: val, multiValue: opts.HasMultiple, } } // ArrayPrependSpecOptions are the options available to subdocument ArrayPrepend operations. type ArrayPrependSpecOptions struct { CreatePath bool IsXattr bool // HasMultiple adds multiple values as elements to an array. // When used `value` in the spec must be an array type // ArrayPrepend("path", []int{1,2,3,4}, ArrayPrependSpecOptions{HasMultiple:true}) => // "path" [1,2,3,4, ....] // // This is a more efficient version (at both the network and server levels) // of doing // spec.ArrayPrepend("path", 1, nil) // spec.ArrayPrepend("path", 2, nil) // spec.ArrayPrepend("path", 3, nil) HasMultiple bool } // ArrayPrependSpec adds an element to the beginning (i.e. left) of an array func ArrayPrependSpec(path string, val interface{}, opts *ArrayPrependSpecOptions) MutateInSpec { if opts == nil { opts = &ArrayPrependSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpArrayPushFirst, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: val, multiValue: opts.HasMultiple, } } // ArrayInsertSpecOptions are the options available to subdocument ArrayInsert operations. type ArrayInsertSpecOptions struct { CreatePath bool IsXattr bool // HasMultiple adds multiple values as elements to an array. // When used `value` in the spec must be an array type // ArrayInsert("path[1]", []int{1,2,3,4}, ArrayInsertSpecOptions{HasMultiple:true}) => // "path" [..., 1,2,3,4] // // This is a more efficient version (at both the network and server levels) // of doing // spec.ArrayInsert("path[2]", 1, nil) // spec.ArrayInsert("path[3]", 2, nil) // spec.ArrayInsert("path[4]", 3, nil) HasMultiple bool } // ArrayInsertSpec inserts an element at a given position within an array. The position should be // specified as part of the path, e.g. path.to.array[3] func ArrayInsertSpec(path string, val interface{}, opts *ArrayInsertSpecOptions) MutateInSpec { if opts == nil { opts = &ArrayInsertSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpArrayInsert, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: val, multiValue: opts.HasMultiple, } } // ArrayAddUniqueSpecOptions are the options available to subdocument ArrayAddUnique operations. type ArrayAddUniqueSpecOptions struct { CreatePath bool IsXattr bool } // ArrayAddUniqueSpec adds an dictionary add unique operation to this mutation operation set. func ArrayAddUniqueSpec(path string, val interface{}, opts *ArrayAddUniqueSpecOptions) MutateInSpec { if opts == nil { opts = &ArrayAddUniqueSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpArrayAddUnique, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: val, multiValue: false, } } // CounterSpecOptions are the options available to subdocument Increment and Decrement operations. type CounterSpecOptions struct { CreatePath bool IsXattr bool } // IncrementSpec adds an increment operation to this mutation operation set. func IncrementSpec(path string, delta int64, opts *CounterSpecOptions) MutateInSpec { if opts == nil { opts = &CounterSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpCounter, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: delta, multiValue: false, } } // DecrementSpec adds a decrement operation to this mutation operation set. func DecrementSpec(path string, delta int64, opts *CounterSpecOptions) MutateInSpec { if opts == nil { opts = &CounterSpecOptions{} } return MutateInSpec{ op: memd.SubDocOpCounter, createPath: opts.CreatePath, isXattr: opts.IsXattr, path: path, value: -delta, multiValue: false, } } gocb-2.6.3/testcluster_test.go000066400000000000000000000325171441755043100164170ustar00rootroot00000000000000package gocb import ( "time" cavescli "github.com/couchbaselabs/gocaves/client" ) var ( srvVer180 = NodeVersion{1, 8, 0, 0, 0, "", false} srvVer200 = NodeVersion{2, 0, 0, 0, 0, "", false} srvVer250 = NodeVersion{2, 5, 0, 0, 0, "", false} srvVer300 = NodeVersion{3, 0, 0, 0, 0, "", false} srvVer400 = NodeVersion{4, 0, 0, 0, 0, "", false} srvVer450 = NodeVersion{4, 5, 0, 0, 0, "", false} srvVer500 = NodeVersion{5, 0, 0, 0, 0, "", false} srvVer550 = NodeVersion{5, 5, 0, 0, 0, "", false} srvVer551 = NodeVersion{5, 5, 1, 0, 0, "", false} srvVer552 = NodeVersion{5, 5, 2, 0, 0, "", false} srvVer553 = NodeVersion{5, 5, 3, 0, 0, "", false} srvVer600 = NodeVersion{6, 0, 0, 0, 0, "", false} srvVer650 = NodeVersion{6, 5, 0, 0, 0, "", false} srvVer650DP = NodeVersion{6, 5, 0, 0, 0, "dp", false} srvVer660 = NodeVersion{6, 6, 0, 0, 0, "", false} srvVer700 = NodeVersion{7, 0, 0, 0, 0, "", false} srvVer710 = NodeVersion{7, 1, 0, 0, 0, "", false} srvVer711 = NodeVersion{7, 1, 1, 0, 0, "", false} srvVer710DP = NodeVersion{7, 1, 0, 0, 0, "dp", false} srvVer720 = NodeVersion{7, 2, 0, 0, 0, "", false} srvVer750 = NodeVersion{7, 5, 0, 0, 0, "", false} mockVer156 = NodeVersion{1, 5, 6, 0, 0, "", true} mockVer1513 = NodeVersion{1, 5, 13, 0, 0, "", true} mockVer1515 = NodeVersion{1, 5, 15, 0, 0, "", true} ) type FeatureCode string var ( KeyValueFeature = FeatureCode("keyvalue") ViewFeature = FeatureCode("view") QueryFeature = FeatureCode("query") ClusterLevelQueryFeature = FeatureCode("clusterQuery") SubdocFeature = FeatureCode("subdoc") RbacFeature = FeatureCode("rbac") SearchFeature = FeatureCode("search") SearchIndexFeature = FeatureCode("searchindex") AnalyticsFeature = FeatureCode("analytics") XattrFeature = FeatureCode("xattrs") CollectionsFeature = FeatureCode("collections") CollectionsManagerMaxCollectionsFeature = FeatureCode("collectionsmgrmaxcollections") CollectionsManagerFeature = FeatureCode("collectionsmgr") AdjoinFeature = FeatureCode("adjoin") ExpandMacrosFeature = FeatureCode("expandmacros") DurabilityFeature = FeatureCode("durability") UserGroupFeature = FeatureCode("usergroup") UserManagerFeature = FeatureCode("usermanager") AnalyticsIndexFeature = FeatureCode("analyticsindex") BucketMgrFeature = FeatureCode("bucketmgr") SearchAnalyzeFeature = FeatureCode("searchanalyze") AnalyticsIndexPendingMutationsFeature = FeatureCode("analyticspending") GetMetaFeature = FeatureCode("getmeta") PingFeature = FeatureCode("ping") ViewIndexUpsertBugFeature = FeatureCode("viewinsertupsertbug") ReplicasFeature = FeatureCode("replicas") PingAnalyticsFeature = FeatureCode("pinganalytics") WaitUntilReadyFeature = FeatureCode("waituntilready") WaitUntilReadyClusterFeature = FeatureCode("waituntilreadycluster") QueryIndexFeature = FeatureCode("queryindex") CollectionsQueryFeature = FeatureCode("collectionsquery") CollectionsAnalyticsFeature = FeatureCode("collectionsanalytics") BucketMgrDurabilityFeature = FeatureCode("bucketmgrdura") AnalyticsIndexLinksFeature = FeatureCode("analyticsindexlinks") AnalyticsIndexLinksScopesFeature = FeatureCode("analyticsindexscopeslinks") EnhancedPreparedStatementsFeature = FeatureCode("enhancedpreparedstatements") PreserveExpiryFeature = FeatureCode("preserveexpiry") EventingFunctionManagerFeature = FeatureCode("eventingmanagement") RateLimitingFeature = FeatureCode("ratelimits") StorageBackendFeature = FeatureCode("storagebackend") HLCFeature = FeatureCode("hlc") TransactionsFeature = FeatureCode("transactions") TransactionsBulkFeature = FeatureCode("transactionsbulk") CustomConflictResolutionFeature = FeatureCode("customconflictresolution") QueryImprovedErrorsFeature = FeatureCode("queryimprovederrors") TransactionsQueryFeature = FeatureCode("transactionsquery") UserManagerChangePasswordFeature = FeatureCode("usermanagerchangepassword") TransactionsRemoveLocationFeature = FeatureCode("transactionsremovelocation") TransactionsSingleQueryExistsErrorFeature = FeatureCode("transactionssinglequeryexists") EventingFunctionManagerMB52649Feature = FeatureCode("eventingmanagementmb52649") EventingFunctionManagerMB52572Feature = FeatureCode("eventingmanagementmb52572") RangeScanFeature = FeatureCode("rangescan") ) type TestFeatureFlag struct { Enabled bool Feature FeatureCode } type testClusterErrorWrap struct { InnerError error Message string } func (e testClusterErrorWrap) Error() string { return e.Message + ": " + e.InnerError.Error() } func (e testClusterErrorWrap) Unwrap() error { return e.InnerError } type testCluster struct { *Cluster Mock *cavescli.Client RunID string Version *NodeVersion FeatureFlags []TestFeatureFlag } func (c *testCluster) isMock() bool { return c.Mock != nil } func (c *testCluster) waitUntilReadyTimeout() time.Duration { if c.Version.Equal(srvVer750) { return 30 * time.Second } else { return 7 * time.Second } } func (c *testCluster) txnCleanupTimeout() time.Duration { if c.Version.Equal(srvVer750) { return 60 * time.Second } else { return 10 * time.Second } } func (c *testCluster) SupportsFeature(feature FeatureCode) bool { featureFlagValue := 0 for _, featureFlag := range c.FeatureFlags { if featureFlag.Feature == feature || featureFlag.Feature == "*" { if featureFlag.Enabled { featureFlagValue = +1 } else { featureFlagValue = -1 } } } if featureFlagValue == -1 { return false } else if featureFlagValue == +1 { return true } supported := false if c.Version.IsMock { supported = true switch feature { case SearchIndexFeature: supported = false case AnalyticsFeature: supported = false case QueryFeature: supported = false case ClusterLevelQueryFeature: supported = false case SearchFeature: supported = false case UserGroupFeature: supported = false case AnalyticsIndexFeature: supported = false case BucketMgrFeature: supported = false case SearchAnalyzeFeature: supported = false case AnalyticsIndexPendingMutationsFeature: supported = false case QueryIndexFeature: supported = false case CollectionsQueryFeature: supported = false case CollectionsAnalyticsFeature: supported = false case BucketMgrDurabilityFeature: supported = false case AnalyticsIndexLinksFeature: supported = false case AnalyticsIndexLinksScopesFeature: supported = false case EnhancedPreparedStatementsFeature: supported = false case PreserveExpiryFeature: supported = false case EventingFunctionManagerFeature: supported = false case RateLimitingFeature: supported = false case StorageBackendFeature: supported = false case TransactionsBulkFeature: supported = false case CustomConflictResolutionFeature: supported = false case QueryImprovedErrorsFeature: supported = false case TransactionsQueryFeature: supported = false case UserManagerChangePasswordFeature: supported = false case TransactionsRemoveLocationFeature: supported = false case TransactionsSingleQueryExistsErrorFeature: supported = false case RangeScanFeature: supported = false } } else { switch feature { case KeyValueFeature: supported = !c.Version.Lower(srvVer180) case ViewFeature: supported = !c.Version.Lower(srvVer200) && !c.Version.Equal(srvVer650DP) && !c.Version.Equal(srvVer750) case QueryFeature: supported = !c.Version.Lower(srvVer400) && !c.Version.Equal(srvVer650DP) case ClusterLevelQueryFeature: supported = !c.Version.Lower(srvVer400) && !c.Version.Equal(srvVer650DP) && !c.Version.Equal(srvVer750) case SubdocFeature: supported = !c.Version.Lower(srvVer450) case XattrFeature: supported = !c.Version.Lower(srvVer450) case RbacFeature: supported = !c.Version.Lower(srvVer500) case SearchFeature: supported = !c.Version.Lower(srvVer500) && !c.Version.Equal(srvVer650DP) case SearchIndexFeature: supported = !c.Version.Lower(srvVer500) && !c.Version.Equal(srvVer650DP) case AnalyticsFeature: supported = !c.Version.Lower(srvVer600) && !c.Version.Equal(srvVer650DP) && !c.Version.Equal(srvVer750) case CollectionsFeature: supported = c.Version.Equal(srvVer650DP) || !c.Version.Lower(srvVer700) case ExpandMacrosFeature: supported = !c.Version.Lower(srvVer450) case AdjoinFeature: supported = !c.Version.Equal(srvVer551) && !c.Version.Equal(srvVer552) && !c.Version.Equal(srvVer553) case DurabilityFeature: supported = !c.Version.Lower(srvVer650) case UserGroupFeature: supported = !c.Version.Lower(srvVer650) && !c.Version.Equal(srvVer750) case UserManagerFeature: supported = !c.Version.Lower(srvVer500) && !c.Version.Equal(srvVer750) case AnalyticsIndexFeature: supported = !c.Version.Lower(srvVer600) && !c.Version.Equal(srvVer650DP) && !c.Version.Equal(srvVer750) case BucketMgrFeature: supported = !c.Version.Equal(srvVer750) case SearchAnalyzeFeature: supported = !c.Version.Lower(srvVer650) && !c.Version.Equal(srvVer650DP) case AnalyticsIndexPendingMutationsFeature: supported = !c.Version.Lower(srvVer650) && !c.Version.Equal(srvVer650DP) case GetMetaFeature: supported = true case PingFeature: supported = !c.Version.Equal(srvVer750) case ViewIndexUpsertBugFeature: supported = !c.Version.Equal(srvVer650) case PingAnalyticsFeature: supported = !c.Version.Lower(srvVer600) case WaitUntilReadyFeature: supported = true case WaitUntilReadyClusterFeature: supported = !c.Version.Lower(srvVer650) && !c.Version.Equal(srvVer750) case ReplicasFeature: supported = true case QueryIndexFeature: supported = !c.Version.Equal(srvVer650DP) case CollectionsQueryFeature: supported = !c.Version.Lower(srvVer700) case CollectionsAnalyticsFeature: supported = !c.Version.Lower(srvVer700) && !c.Version.Equal(srvVer750) case CollectionsManagerFeature: supported = !c.Version.Lower(srvVer700) case CollectionsManagerMaxCollectionsFeature: supported = !c.Version.Lower(srvVer700) case BucketMgrDurabilityFeature: supported = !c.Version.Lower(srvVer660) case AnalyticsIndexLinksFeature: supported = !c.Version.Lower(srvVer660) case AnalyticsIndexLinksScopesFeature: supported = !c.Version.Lower(srvVer700) case EnhancedPreparedStatementsFeature: supported = !c.Version.Lower(srvVer650) case PreserveExpiryFeature: supported = !c.Version.Lower(srvVer700) case EventingFunctionManagerFeature: supported = !c.Version.Lower(srvVer700) && !c.Version.Equal(srvVer750) case RateLimitingFeature: supported = !c.Version.Lower(srvVer710) && c.Version.Lower(srvVer720) case StorageBackendFeature: supported = !c.Version.Lower(srvVer710) && (c.Version.Edition != CommunityNodeEdition) case HLCFeature: supported = !c.Version.Lower(srvVer660) case TransactionsFeature: supported = !c.Version.Lower(srvVer700) case TransactionsQueryFeature: supported = !c.Version.Lower(srvVer700) case TransactionsBulkFeature: supported = !c.Version.Lower(srvVer700) case CustomConflictResolutionFeature: supported = c.Version.Equal(srvVer710DP) case QueryImprovedErrorsFeature: supported = !c.Version.Lower(srvVer710) case UserManagerChangePasswordFeature: supported = !c.Version.Lower(srvVer600) case TransactionsRemoveLocationFeature: supported = !c.Version.Lower(srvVer700) case TransactionsSingleQueryExistsErrorFeature: supported = !c.Version.Lower(srvVer710) case EventingFunctionManagerMB52649Feature: supported = !c.Version.Equal(srvVer711) case EventingFunctionManagerMB52572Feature: supported = !c.Version.Equal(srvVer711) case RangeScanFeature: supported = !c.Version.Lower(srvVer750) } } return supported } func (c *testCluster) NotSupportsFeature(feature FeatureCode) bool { return !c.SupportsFeature(feature) } func (c *testCluster) TimeTravel(waitDura time.Duration) { if c.Mock != nil { c.Mock.TimeTravelRun(c.RunID, waitDura) } else { time.Sleep(waitDura) } } func (c *testCluster) DefaultCollection(bucket *Bucket) *Collection { return bucket.DefaultCollection() } func (c *testCluster) CreateBreweryDataset(col *Collection) error { var dataset []testBreweryDocument err := loadJSONTestDataset("beer_sample_brewery_five", &dataset) if err != nil { return testClusterErrorWrap{ InnerError: err, Message: "could not read test dataset"} } for _, doc := range dataset { _, err = col.Upsert(doc.Name, doc, nil) if err != nil { return testClusterErrorWrap{ InnerError: err, Message: "could not create dataset"} } } return nil } gocb-2.6.3/testdata/000077500000000000000000000000001441755043100142515ustar00rootroot00000000000000gocb-2.6.3/testdata/analytics_timeout.json000066400000000000000000000006051441755043100207020ustar00rootroot00000000000000{ "requestID": "74ed0ec2-8699-417d-b807-599520b70b4c", "signature": { "*": "*" }, "errors": [{ "code": 21002, "msg": "Request timed out and will be cancelled" }], "status": "timeout", "metrics": { "elapsedTime": "24.187696ms", "executionTime": "19.366336ms", "resultCount": 0, "resultSize": 0, "processedObjects": 0, "errorCount": 1 } } gocb-2.6.3/testdata/beer_sample_analytics_dataset.json000066400000000000000000000105571441755043100232060ustar00rootroot00000000000000{ "requestID": "30f6bcdf-2288-4fe1-bea1-361bb96984a4", "signature": { "*": "*" }, "results": [ { "address": [ "407 Radam, F200" ], "city": "Austin", "code": "78745", "country": "United States", "description": "(512) Brewing Company is a microbrewery located in the heart of Austin that brews for the community using as many local, domestic and organic ingredients as possible.", "geo": { "accuracy": "ROOFTOP", "lat": 30.2234, "lon": -97.7697 }, "name": "(512) Brewing Company", "phone": "512.707.2337", "state": "Texas", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://512brewing.com/" }, { "address": [ "563 Second Street" ], "city": "San Francisco", "code": "94107", "country": "United States", "description": "The 21st Amendment Brewery offers a variety of award winning house made brews and American grilled cuisine in a comfortable loft like setting. Join us before and after Giants baseball games in our outdoor beer garden. A great location for functions and parties in our semi-private Brewers Loft. See you soon at the 21A!", "geo": { "accuracy": "ROOFTOP", "lat": 37.7825, "lon": -122.393 }, "name": "21st Amendment Brewery Cafe", "phone": "1-415-369-0900", "state": "California", "type": "brewery", "updated": "2010-10-24 13:54:07", "website": "http://www.21st-amendment.com/" }, { "address": [ "Hoogstraat 2A" ], "city": "Beersel", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.7668, "lon": 4.3081 }, "name": "3 Fonteinen Brouwerij Ambachtelijke Geuzestekerij", "phone": "32-02-/-306-71-03", "state": "Vlaams Brabant", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.3fonteinen.be/index.htm" }, { "address": [ "Ole Steensgt. 10 Postboks 1530" ], "city": "Drammen", "code": "", "country": "Norway", "description": "Aass Brewery was established in 1834 and is the oldest brewery in Norway today. It is located in the city of Drammen, approximately 25 miles south of our capital, Oslo. You will spot it at the banks of the Drammen River at the very same place as it has been since 1834. The annual production of beer is aprox. 10 mill liter (85 000 barrels), and together with the production of 18 mill liters of soft drinks and mineralwater it gives employment to approximately 150 people. You may wonder how we got our name ? It was a young fellow with the name Poul Lauritz Aass that bought the brewery in 1860. Since then it has been in the same family, and is today run by the 4th generation of Aass. (By the way - A real Norwegian viking would pronounce it Ouse) The brewery is proud of its history. To us the quality of what we produce has always been and always will be the main issue. In 1516, Duke Wilhelm of Bavaria established a law admitting only malted barley, hops yeast, and water as ingredients in the production of beer. The law is known as the purity law and Aass Brewery is still loyal to Duke Wilhelm of Bavaria. In Norway the brewery is known for outstanding quality and large number of different beer types.", "geo": { "accuracy": "APPROXIMATE", "lat": 59.7451, "lon": 10.2135 }, "name": "Aass Brewery", "phone": "47-32-26-60-00", "state": "", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.aass.no" }, { "address": [ "Rue de l'Abbaye 8" ], "city": "Rochefort", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.1999, "lon": 5.2277 }, "name": "Abbaye Notre Dame du St Remy", "phone": "32-084/22.01.47", "state": "Namur", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "" } ], "plans": {}, "status": "success", "metrics": { "elapsedTime": "258.345597ms", "executionTime": "251.536764ms", "resultCount": 5, "resultSize": 3367, "processedObjects": 26 } } gocb-2.6.3/testdata/beer_sample_analytics_temp_error.json000066400000000000000000000006241441755043100237310ustar00rootroot00000000000000{ "requestID": "fbe9ac66-a7ed-4b09-b1dc-4d3c791d8953", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "errors": [ { "code": 23000, "msg": "Analytics Service is temporarily unavailable" } ], "status": "errors", "metrics": { "elapsedTime": "837.425µs", "executionTime": "732.345µs", "resultCount": 0, "resultSize": 0, "errorCount": 1 } } gocb-2.6.3/testdata/beer_sample_brewery_five.json000066400000000000000000000075571441755043100222100ustar00rootroot00000000000000[ { "address": [ "407 Radam, F200" ], "city": "Austin", "code": "78745", "country": "United States", "description": "(512) Brewing Company is a microbrewery located in the heart of Austin that brews for the community using as many local, domestic and organic ingredients as possible.", "geo": { "accuracy": "ROOFTOP", "lat": 30.2234, "lon": -97.7697 }, "name": "(512) Brewing Company", "phone": "512.707.2337", "state": "Texas", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://512brewing.com/" }, { "address": [ "563 Second Street" ], "city": "San Francisco", "code": "94107", "country": "United States", "description": "The 21st Amendment Brewery offers a variety of award winning house made brews and American grilled cuisine in a comfortable loft like setting. Join us before and after Giants baseball games in our outdoor beer garden. A great location for functions and parties in our semi-private Brewers Loft. See you soon at the 21A!", "geo": { "accuracy": "ROOFTOP", "lat": 37.7825, "lon": -122.393 }, "name": "21st Amendment Brewery Cafe", "phone": "1-415-369-0900", "state": "California", "type": "brewery", "updated": "2010-10-24 13:54:07", "website": "http://www.21st-amendment.com/" }, { "address": [ "Hoogstraat 2A" ], "city": "Beersel", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.7668, "lon": 4.3081 }, "name": "3 Fonteinen Brouwerij Ambachtelijke Geuzestekerij", "phone": "32-02-/-306-71-03", "state": "Vlaams Brabant", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.3fonteinen.be/index.htm" }, { "address": [ "Ole Steensgt. 10 Postboks 1530" ], "city": "Drammen", "code": "", "country": "Norway", "description": "Aass Brewery was established in 1834 and is the oldest brewery in Norway today. It is located in the city of Drammen, approximately 25 miles south of our capital, Oslo. You will spot it at the banks of the Drammen River at the very same place as it has been since 1834. The annual production of beer is aprox. 10 mill liter (85 000 barrels), and together with the production of 18 mill liters of soft drinks and mineralwater it gives employment to approximately 150 people. You may wonder how we got our name ? It was a young fellow with the name Poul Lauritz Aass that bought the brewery in 1860. Since then it has been in the same family, and is today run by the 4th generation of Aass. (By the way - A real Norwegian viking would pronounce it Ouse) The brewery is proud of its history. To us the quality of what we produce has always been and always will be the main issue. In 1516, Duke Wilhelm of Bavaria established a law admitting only malted barley, hops yeast, and water as ingredients in the production of beer. The law is known as the purity law and Aass Brewery is still loyal to Duke Wilhelm of Bavaria. In Norway the brewery is known for outstanding quality and large number of different beer types.", "geo": { "accuracy": "APPROXIMATE", "lat": 59.7451, "lon": 10.2135 }, "name": "Aass Brewery", "phone": "47-32-26-60-00", "state": "", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.aass.no" }, { "address": [ "Rue de l'Abbaye 8" ], "city": "Rochefort", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.1999, "lon": 5.2277 }, "name": "Abbaye Notre Dame du St Remy", "phone": "32-084/22.01.47", "state": "Namur", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "" } ] gocb-2.6.3/testdata/beer_sample_brewery_single.json000066400000000000000000000005031441755043100225200ustar00rootroot00000000000000{ "city": "Portland", "code": "", "country": "United States", "description": "", "geo": { "accuracy": "APPROXIMATE", "lat": 45.5325, "lon": -122.686 }, "name": "Blitz-Weinhard Brewing", "phone": "", "state": "Oregon", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "" } gocb-2.6.3/testdata/beer_sample_query_dataset.json000066400000000000000000000105741441755043100223630ustar00rootroot00000000000000{ "requestID": "e36e0202-7f4f-4083-9b73-993459353544", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "signature": { "*": "*" }, "results": [ { "address": [ "407 Radam, F200" ], "city": "Austin", "code": "78745", "country": "United States", "description": "(512) Brewing Company is a microbrewery located in the heart of Austin that brews for the community using as many local, domestic and organic ingredients as possible.", "geo": { "accuracy": "ROOFTOP", "lat": 30.2234, "lon": -97.7697 }, "name": "(512) Brewing Company", "phone": "512.707.2337", "state": "Texas", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://512brewing.com/" }, { "address": [ "563 Second Street" ], "city": "San Francisco", "code": "94107", "country": "United States", "description": "The 21st Amendment Brewery offers a variety of award winning house made brews and American grilled cuisine in a comfortable loft like setting. Join us before and after Giants baseball games in our outdoor beer garden. A great location for functions and parties in our semi-private Brewers Loft. See you soon at the 21A!", "geo": { "accuracy": "ROOFTOP", "lat": 37.7825, "lon": -122.393 }, "name": "21st Amendment Brewery Cafe", "phone": "1-415-369-0900", "state": "California", "type": "brewery", "updated": "2010-10-24 13:54:07", "website": "http://www.21st-amendment.com/" }, { "address": [ "Hoogstraat 2A" ], "city": "Beersel", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.7668, "lon": 4.3081 }, "name": "3 Fonteinen Brouwerij Ambachtelijke Geuzestekerij", "phone": "32-02-/-306-71-03", "state": "Vlaams Brabant", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.3fonteinen.be/index.htm" }, { "address": [ "Ole Steensgt. 10 Postboks 1530" ], "city": "Drammen", "code": "", "country": "Norway", "description": "Aass Brewery was established in 1834 and is the oldest brewery in Norway today. It is located in the city of Drammen, approximately 25 miles south of our capital, Oslo. You will spot it at the banks of the Drammen River at the very same place as it has been since 1834. The annual production of beer is aprox. 10 mill liter (85 000 barrels), and together with the production of 18 mill liters of soft drinks and mineralwater it gives employment to approximately 150 people. You may wonder how we got our name ? It was a young fellow with the name Poul Lauritz Aass that bought the brewery in 1860. Since then it has been in the same family, and is today run by the 4th generation of Aass. (By the way - A real Norwegian viking would pronounce it Ouse) The brewery is proud of its history. To us the quality of what we produce has always been and always will be the main issue. In 1516, Duke Wilhelm of Bavaria established a law admitting only malted barley, hops yeast, and water as ingredients in the production of beer. The law is known as the purity law and Aass Brewery is still loyal to Duke Wilhelm of Bavaria. In Norway the brewery is known for outstanding quality and large number of different beer types.", "geo": { "accuracy": "APPROXIMATE", "lat": 59.7451, "lon": 10.2135 }, "name": "Aass Brewery", "phone": "47-32-26-60-00", "state": "", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.aass.no" }, { "address": [ "Rue de l'Abbaye 8" ], "city": "Rochefort", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.1999, "lon": 5.2277 }, "name": "Abbaye Notre Dame du St Remy", "phone": "32-084/22.01.47", "state": "Namur", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "" } ], "status": "success", "metrics": { "elapsedTime": "9.64915ms", "executionTime": "9.58744ms", "resultCount": 10, "resultSize": 4348 } } gocb-2.6.3/testdata/beer_sample_query_dataset_no_metrics.json000066400000000000000000000103701441755043100245770ustar00rootroot00000000000000{ "requestID": "e36e0202-7f4f-4083-9b73-993459353544", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "signature": { "*": "*" }, "results": [ { "address": [ "407 Radam, F200" ], "city": "Austin", "code": "78745", "country": "United States", "description": "(512) Brewing Company is a microbrewery located in the heart of Austin that brews for the community using as many local, domestic and organic ingredients as possible.", "geo": { "accuracy": "ROOFTOP", "lat": 30.2234, "lon": -97.7697 }, "name": "(512) Brewing Company", "phone": "512.707.2337", "state": "Texas", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://512brewing.com/" }, { "address": [ "563 Second Street" ], "city": "San Francisco", "code": "94107", "country": "United States", "description": "The 21st Amendment Brewery offers a variety of award winning house made brews and American grilled cuisine in a comfortable loft like setting. Join us before and after Giants baseball games in our outdoor beer garden. A great location for functions and parties in our semi-private Brewers Loft. See you soon at the 21A!", "geo": { "accuracy": "ROOFTOP", "lat": 37.7825, "lon": -122.393 }, "name": "21st Amendment Brewery Cafe", "phone": "1-415-369-0900", "state": "California", "type": "brewery", "updated": "2010-10-24 13:54:07", "website": "http://www.21st-amendment.com/" }, { "address": [ "Hoogstraat 2A" ], "city": "Beersel", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.7668, "lon": 4.3081 }, "name": "3 Fonteinen Brouwerij Ambachtelijke Geuzestekerij", "phone": "32-02-/-306-71-03", "state": "Vlaams Brabant", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.3fonteinen.be/index.htm" }, { "address": [ "Ole Steensgt. 10 Postboks 1530" ], "city": "Drammen", "code": "", "country": "Norway", "description": "Aass Brewery was established in 1834 and is the oldest brewery in Norway today. It is located in the city of Drammen, approximately 25 miles south of our capital, Oslo. You will spot it at the banks of the Drammen River at the very same place as it has been since 1834. The annual production of beer is aprox. 10 mill liter (85 000 barrels), and together with the production of 18 mill liters of soft drinks and mineralwater it gives employment to approximately 150 people. You may wonder how we got our name ? It was a young fellow with the name Poul Lauritz Aass that bought the brewery in 1860. Since then it has been in the same family, and is today run by the 4th generation of Aass. (By the way - A real Norwegian viking would pronounce it Ouse) The brewery is proud of its history. To us the quality of what we produce has always been and always will be the main issue. In 1516, Duke Wilhelm of Bavaria established a law admitting only malted barley, hops yeast, and water as ingredients in the production of beer. The law is known as the purity law and Aass Brewery is still loyal to Duke Wilhelm of Bavaria. In Norway the brewery is known for outstanding quality and large number of different beer types.", "geo": { "accuracy": "APPROXIMATE", "lat": 59.7451, "lon": 10.2135 }, "name": "Aass Brewery", "phone": "47-32-26-60-00", "state": "", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.aass.no" }, { "address": [ "Rue de l'Abbaye 8" ], "city": "Rochefort", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.1999, "lon": 5.2277 }, "name": "Abbaye Notre Dame du St Remy", "phone": "32-084/22.01.47", "state": "Namur", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "" } ], "status": "success" } gocb-2.6.3/testdata/beer_sample_query_error.json000066400000000000000000000005711441755043100220630ustar00rootroot00000000000000{ "requestID": "fbe9ac66-a7ed-4b09-b1dc-4d3c791d8953", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "errors": [ { "code": 3000, "msg": "syntax error - at *" } ], "status": "fatal", "metrics": { "elapsedTime": "837.425µs", "executionTime": "732.345µs", "resultCount": 0, "resultSize": 0, "errorCount": 1 } } gocb-2.6.3/testdata/beer_sample_query_temp_error.json000066400000000000000000000005661441755043100231140ustar00rootroot00000000000000{ "requestID": "fbe9ac66-a7ed-4b09-b1dc-4d3c791d8953", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "errors": [ { "code": 4050, "msg": "temporary error" } ], "status": "errors", "metrics": { "elapsedTime": "837.425µs", "executionTime": "732.345µs", "resultCount": 0, "resultSize": 0, "errorCount": 1 } } gocb-2.6.3/testdata/beer_sample_query_timeout.json000066400000000000000000000006621441755043100224210ustar00rootroot00000000000000{ "requestID": "59bf7bff-f190-4132-9e5b-4f4564d9b269", "clientContextID": "9f6b7330-4623-4123-b340-d991594a90af", "signature": { "type":"json" }, "results": [ ], "errors": [ {"code":1080, "msg":"Timeout 50ms exceeded" } ], "status": "timeout", "metrics": { "elapsedTime": "20.323957ms", "executionTime": "20.237546ms", "resultCount": 0, "resultSize": 0, "errorCount": 1 } } gocb-2.6.3/testdata/beer_sample_search_dataset.json000066400000000000000000000032451441755043100224600ustar00rootroot00000000000000{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": { "query": { "query": "London" }, "size": 5, "from": 0, "highlight": null, "fields": null, "facets": null, "explain": false, "sort": [ "-_score" ], "includeLocations": false }, "hits": [ { "index": "hotels_384418a18e59eba2_acbbef99", "id": "landmark_37138", "score": 1.156383395549805, "sort": [ "_score" ], "fields": { "name": "landmark_37138" } }, { "index": "hotels_384418a18e59eba2_acbbef99", "id": "landmark_16309", "score": 1.1315120632881468, "sort": [ "_score" ], "fields": { "name": "landmark_16309" } }, { "index": "hotels_384418a18e59eba2_acbbef99", "id": "landmark_16073", "score": 1.1059247262932046, "sort": [ "_score" ], "fields": { "name": "landmark_16073" } }, { "index": "hotels_384418a18e59eba2_acbbef99", "id": "landmark_16288", "score": 1.0926796273206518, "sort": [ "_score" ], "fields": { "name": "landmark_16288" } }, { "index": "hotels_384418a18e59eba2_acbbef99", "id": "landmark_16071", "score": 1.0798993013885354, "sort": [ "_score" ], "fields": { "name": "landmark_16071" } } ], "total_hits": 809, "max_score": 1.156383395549805, "took": 62511375, "facets": { "type": { "name": "", "field": "country", "total": 7, "missing": 0, "other": 0 } } } gocb-2.6.3/testdata/beer_sample_single.json000066400000000000000000000012251441755043100207630ustar00rootroot00000000000000{ "abv": 7.6, "brewery_id": "512_brewing_company", "category": "North American Ale", "description": "At once cuddly and ferocious, (512) BRUIN combines a smooth, rich maltiness and mahogany color with a solid hop backbone and stealthy 7.6% alcohol. Made with Organic 2 Row and Munich malts, plus Chocolate and Crystal malts, domestic hops, and a touch of molasses, this brew has notes of raisins, dark sugars, and cocoa, and pairs perfectly with food and the crisp fall air.", "ibu": 0, "name": "(512) Bruin", "srm": 0, "style": "American-Style Brown Ale", "type": "beer", "upc": 0, "updated": "2010-07-22T20:00:20Z", "profile": { } } gocb-2.6.3/testdata/beer_sample_views_dataset.json000066400000000000000000000153471441755043100223560ustar00rootroot00000000000000{ "debug_info": { "local": { "main_group": { "active_partitions": [ 0, 1, 2, 3, 4, 5, 6, 7 ], "original_active_partitions": [ 0, 1, 2, 3, 4, 5, 6, 7 ], "passive_partitions": [], "original_passive_partitions": [], "cleanup_partitions": [], "replica_partitions": [], "replicas_on_transfer": [], "indexable_seqs": { "0000": 0, "0001": 0, "0002": 0, "0003": 0, "0004": 0, "0005": 0, "0006": 0, "0007": 0 }, "unindexeable_seqs": {}, "wanted_seqs": {}, "wanted_partitions": [ 0, 1, 2, 3, 4, 5, 6, 7 ], "pending_transition": null, "stats": { "dup_partitions_counter": 0, "cleanup_history": [], "compaction_history": [], "update_history": [ { "indexing_time": 0.043028, "blocked_time": 0.000109, "cleanup_kv_count": 0, "inserted_ids": 5, "deleted_ids": 0, "inserted_kvs": 5, "deleted_kvs": 0 }, { "indexing_time": 0.048076, "blocked_time": 0.000161, "cleanup_kv_count": 0, "inserted_ids": 5, "deleted_ids": 0, "inserted_kvs": 5, "deleted_kvs": 0 }, { "indexing_time": 0.183162, "blocked_time": 0.000119, "cleanup_kv_count": 0, "inserted_ids": 10, "deleted_ids": 0, "inserted_kvs": 5, "deleted_kvs": 0 } ], "update_errors": 0, "updater_cleanups": 0, "cleanups": 0, "cleanup_stops": 0, "compactions": 0, "stopped_updates": 0, "partial_updates": 0, "full_updates": 3, "accesses": 5 } } } }, "total_rows": 5, "rows": [ { "id": "views0", "key": "views0", "partition": 790, "node": "local", "value": { "city": "Austin", "code": "78745", "country": "United States", "description": "(512) Brewing Company is a microbrewery located in the heart of Austin that brews for the community using as many local, domestic and organic ingredients as possible.", "geo": { "accuracy": "ROOFTOP", "lat": 30.2234, "lon": -97.7697 }, "name": "(512) Brewing Company", "phone": "512.707.2337", "state": "Texas", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://512brewing.com/", "service": "views" } }, { "id": "views1", "key": "views1", "partition": 17, "node": "local", "value": { "city": "San Francisco", "code": "94107", "country": "United States", "description": "The 21st Amendment Brewery offers a variety of award winning house made brews and American grilled cuisine in a comfortable loft like setting. Join us before and after Giants baseball games in our outdoor beer garden. A great location for functions and parties in our semi-private Brewers Loft. See you soon at the 21A!", "geo": { "accuracy": "ROOFTOP", "lat": 37.7825, "lon": -122.393 }, "name": "21st Amendment Brewery Cafe", "phone": "1-415-369-0900", "state": "California", "type": "brewery", "updated": "2010-10-24 13:54:07", "website": "http://www.21st-amendment.com/", "service": "views" } }, { "id": "views2", "key": "views2", "partition": 280, "node": "local", "value": { "city": "Beersel", "country": "Belgium", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.7668, "lon": 4.3081 }, "name": "3 Fonteinen Brouwerij Ambachtelijke Geuzestekerij", "phone": "32-02-/-306-71-03", "state": "Vlaams Brabant", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.3fonteinen.be/index.htm", "service": "views" } }, { "id": "views3", "key": "views3", "partition": 543, "node": "local", "value": { "city": "Drammen", "country": "Norway", "description": "Aass Brewery was established in 1834 and is the oldest brewery in Norway today. It is located in the city of Drammen, approximately 25 miles south of our capital, Oslo. You will spot it at the banks of the Drammen River at the very same place as it has been since 1834. The annual production of beer is aprox. 10 mill liter (85 000 barrels), and together with the production of 18 mill liters of soft drinks and mineralwater it gives employment to approximately 150 people. You may wonder how we got our name ? It was a young fellow with the name Poul Lauritz Aass that bought the brewery in 1860. Since then it has been in the same family, and is today run by the 4th generation of Aass. (By the way - A real Norwegian viking would pronounce it Ouse) The brewery is proud of its history. To us the quality of what we produce has always been and always will be the main issue. In 1516, Duke Wilhelm of Bavaria established a law admitting only malted barley, hops yeast, and water as ingredients in the production of beer. The law is known as the purity law and Aass Brewery is still loyal to Duke Wilhelm of Bavaria. In Norway the brewery is known for outstanding quality and large number of different beer types.", "geo": { "accuracy": "APPROXIMATE", "lat": 59.7451, "lon": 10.2135 }, "name": "Aass Brewery", "phone": "47-32-26-60-00", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.aass.no", "service": "views" } }, { "id": "views4", "key": "views4", "partition": 123, "node": "local", "value": { "city": "Rochefort", "country": "Belgium", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.1999, "lon": 5.2277 }, "name": "Abbaye Notre Dame du St Remy", "phone": "32-084/22.01.47", "state": "Namur", "type": "brewery", "updated": "2010-07-22 20:00:20", "service": "views" } } ] } gocb-2.6.3/testdata/enhanced_beer_sample_query_dataset.json000066400000000000000000000107021441755043100242010ustar00rootroot00000000000000{ "requestID": "e36e0202-7f4f-4083-9b73-993459353544", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "signature": { "*": "*" }, "prepared": "[127.0.0.1:8091]adbf974b-9aed-5d37-9c89-3c86b69f9bd4", "results": [ { "address": [ "407 Radam, F200" ], "city": "Austin", "code": "78745", "country": "United States", "description": "(512) Brewing Company is a microbrewery located in the heart of Austin that brews for the community using as many local, domestic and organic ingredients as possible.", "geo": { "accuracy": "ROOFTOP", "lat": 30.2234, "lon": -97.7697 }, "name": "(512) Brewing Company", "phone": "512.707.2337", "state": "Texas", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://512brewing.com/" }, { "address": [ "563 Second Street" ], "city": "San Francisco", "code": "94107", "country": "United States", "description": "The 21st Amendment Brewery offers a variety of award winning house made brews and American grilled cuisine in a comfortable loft like setting. Join us before and after Giants baseball games in our outdoor beer garden. A great location for functions and parties in our semi-private Brewers Loft. See you soon at the 21A!", "geo": { "accuracy": "ROOFTOP", "lat": 37.7825, "lon": -122.393 }, "name": "21st Amendment Brewery Cafe", "phone": "1-415-369-0900", "state": "California", "type": "brewery", "updated": "2010-10-24 13:54:07", "website": "http://www.21st-amendment.com/" }, { "address": [ "Hoogstraat 2A" ], "city": "Beersel", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.7668, "lon": 4.3081 }, "name": "3 Fonteinen Brouwerij Ambachtelijke Geuzestekerij", "phone": "32-02-/-306-71-03", "state": "Vlaams Brabant", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.3fonteinen.be/index.htm" }, { "address": [ "Ole Steensgt. 10 Postboks 1530" ], "city": "Drammen", "code": "", "country": "Norway", "description": "Aass Brewery was established in 1834 and is the oldest brewery in Norway today. It is located in the city of Drammen, approximately 25 miles south of our capital, Oslo. You will spot it at the banks of the Drammen River at the very same place as it has been since 1834. The annual production of beer is aprox. 10 mill liter (85 000 barrels), and together with the production of 18 mill liters of soft drinks and mineralwater it gives employment to approximately 150 people. You may wonder how we got our name ? It was a young fellow with the name Poul Lauritz Aass that bought the brewery in 1860. Since then it has been in the same family, and is today run by the 4th generation of Aass. (By the way - A real Norwegian viking would pronounce it Ouse) The brewery is proud of its history. To us the quality of what we produce has always been and always will be the main issue. In 1516, Duke Wilhelm of Bavaria established a law admitting only malted barley, hops yeast, and water as ingredients in the production of beer. The law is known as the purity law and Aass Brewery is still loyal to Duke Wilhelm of Bavaria. In Norway the brewery is known for outstanding quality and large number of different beer types.", "geo": { "accuracy": "APPROXIMATE", "lat": 59.7451, "lon": 10.2135 }, "name": "Aass Brewery", "phone": "47-32-26-60-00", "state": "", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "http://www.aass.no" }, { "address": [ "Rue de l'Abbaye 8" ], "city": "Rochefort", "code": "", "country": "Belgium", "description": "", "geo": { "accuracy": "RANGE_INTERPOLATED", "lat": 50.1999, "lon": 5.2277 }, "name": "Abbaye Notre Dame du St Remy", "phone": "32-084/22.01.47", "state": "Namur", "type": "brewery", "updated": "2010-07-22 20:00:20", "website": "" } ], "status": "success", "metrics": { "elapsedTime": "9.64915ms", "executionTime": "9.58744ms", "resultCount": 10, "resultSize": 4348 } } gocb-2.6.3/testdata/ping_result.json000066400000000000000000000026471441755043100175100ustar00rootroot00000000000000{ "version": 1, "id": "0xdeadbeef userstring", "config_rev": 53, "sdk": "gocbcore/v7.0.2", "services": { "fts": [ { "id": "0x1415F11", "latency_us": 877909, "remote": "centos7-lx1.home.ingenthron.org:8094", "local": "127.0.0.1:54669", "state": "ok" } ], "kv": [ { "id": "0x1415F12", "latency_us": 1182000, "remote": "centos7-lx1.home.ingenthron.org:11210", "local": "127.0.0.1:54670", "state": "ok", "scope" : "bucketname" } ], "n1ql": [ { "id": "0x1415F13", "latency_us": 6217, "remote": "centos7-lx1.home.ingenthron.org:8093", "local": "127.0.0.1:54671", "state": "ok" }, { "id": "0x1415F14", "latency_us": 2213, "remote": "centos7-lx2.home.ingenthron.org:8095", "local": "127.0.0.1:54682", "state": "timeout" } ], "cbas": [ { "id": "0x1415F15", "latency_us": 2213, "remote": "centos7-lx1.home.ingenthron.org:8095", "local": "127.0.0.1:54675", "state": "error", "details": "endpoint returned HTTP code 500!" } ], "view": [ { "id": "0x1415F16", "latency_us": 45585, "remote": "centos7-lx1.home.ingenthron.org:8092", "local": "127.0.0.1:54672", "state": "ok" } ] } } gocb-2.6.3/testdata/prepared_beer_sample_query_dataset.json000066400000000000000000000063151441755043100242430ustar00rootroot00000000000000{ "requestID": "a2c943b7-2950-432d-823b-4587ecabfe54", "clientContextID": "c6766223-4fb9-4369-96c1-4e7b28d0cf68", "signature": "json", "results": [ { "encoded_plan": "H4sIAAAAAAAA/6STUWvbMBSF/4o5eysKdG1CYu0plBQGGTNJ2csIrmJfO1pl2buSQ7Lg/fYhJ6EkHWNsj7Y+H93zyTqAbFbnlKeNURYSEChI+Zbpobaea+MgbwW0zWk3bfQXYqdrC3kvYFVFkMjfx9l6vB4O7iYxDYZ3xWQQr0frQZzlt3GhJuMh3UOgboiVrxnygHevD1jS95ZsRhD4mW20yZks5NdLaNr6Tc36R6Aa1lttqCQXouba+R5/UlySD/NQoVrjpWe1JTNwqmpM+C5hvYUcd6vuvNM/jfJXUMK6UrxfZsqG7r2942hpc1w6v00brr9R5nupB5xW0xfaQ3puqRN4ob1rVBZcX3cyutKh8wjH4zhzJwcQaJ22JSRKp9GJyykfyWcb/HGD34VexySKlTFk8F9eP1rttTLJUQcEmFxrfOqJK9fTtGsC6MgUEHBe8UnR6k0xbV+TulX3lpj34sQ5c9RTV8zSM6kK/Q/jdGn7WxG63UDiBp2Ap12wnyxmyXQxi5az+ezhKbqJHhefP0XPFy6fo/6sotEHdL8CAAD//5NLCoZ1AwAA", "featureControls": 0, "indexApiVersion": 3, "name": "[127.0.0.1:8091]d19cb7b4-289e-42f8-9b5b-9cd09fa874e3", "operator": { "#operator": "Sequence", "~children": [ { "#operator": "Authorize", "privileges": { "List": [ { "Priv": 7, "Target": "default:travel-sample" } ] }, "~child": { "#operator": "Sequence", "~children": [ { "#operator": "Sequence", "~children": [ { "#operator": "PrimaryScan3", "index": "def_primary", "index_projection": { "primary_key": true }, "keyspace": "travel-sample", "limit": "5", "namespace": "default", "using": "gsi" }, { "#operator": "Fetch", "keyspace": "travel-sample", "namespace": "default" }, { "#operator": "Parallel", "~child": { "#operator": "Sequence", "~children": [ { "#operator": "InitialProject", "result_terms": [ { "expr": "self", "star": true } ] }, { "#operator": "FinalProject" } ] } } ] }, { "#operator": "Limit", "expr": "5" } ] } }, { "#operator": "Stream" } ] }, "signature": { "*": "*" }, "text": "PREPARE SELECT * FROM `travel-sample` limit 5;" } ], "status": "success", "metrics": { "elapsedTime": "13.491479ms", "executionTime": "13.376689ms", "resultCount": 1, "resultSize": 1473 } } gocb-2.6.3/testdata/projection_doc.json000066400000000000000000000022401441755043100201430ustar00rootroot00000000000000{ "name": "Emmy-lou Dickerson", "age": 26, "animals": ["cat", "dog", "parrot"], "attributes": { "hair": "brown", "dimensions": { "height": 67, "weight": 175 }, "hobbies": [ { "type": "winter sports", "name": "curling" }, { "type": "summer sports", "name": "water skiing", "details": { "location": { "lat": 49.282730, "long": -123.120735 } } } ] }, "tracking": { "locations": [ [ { "lat": 49.282730, "long": -123.120735 }, { "lat": 49.282330, "long": -123.120345 } ], [ { "lat": 50.282730, "long": -121.120735 }, { "lat": 29.282330, "long": -126.120345 } ], [ { "lat": 49.282130, "long": -121.120735 }, { "lat": 4.282330, "long": -1.120345 } ] ], "raw": [ [49.282730, -123.120735], [50.282730, -121.120735], [49.282130, -1.120345] ] } } gocb-2.6.3/testdata/query_index_response.json000066400000000000000000000011561441755043100214210ustar00rootroot00000000000000{ "requestID": "a2c943b7-2950-432d-823b-4587ecabfe54", "clientContextID": "c6766223-4fb9-4369-96c1-4e7b28d0cf68", "signature": "*:*", "results": [ { "datastore_id": "http://127.0.0.1:8091", "id": "c8b609c6d50798e0", "index_key": [ "`_type`" ], "keyspace_id": "test", "name": "ih", "namespace_id": "default", "partition": "HASH(`_type`)", "state": "online", "using": "gsi" } ], "status": "success", "metrics": { "elapsedTime": "13.491479ms", "executionTime": "13.376689ms", "resultCount": 1, "resultSize": 1473 } } gocb-2.6.3/testdata/search_analyzedoc.json000066400000000000000000000043571441755043100206330ustar00rootroot00000000000000{ "status": "ok", "analyzed": [ { "couchbase blr": { "Term": "Y291Y2hiYXNlIGJscg==", "Locations": [ { "Field": "title", "ArrayPositions": [], "Start": 0, "End": 13, "Position": 1 } ] } }, { "he": { "Term": "aGU=", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 0, "End": 5, "Position": 1 } ] }, "hel": { "Term": "aGVs", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 0, "End": 5, "Position": 1 } ] }, "hell": { "Term": "aGVsbA==", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 0, "End": 5, "Position": 1 } ] }, "hello": { "Term": "aGVsbG8=", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 0, "End": 5, "Position": 1 } ] }, "wo": { "Term": "d28=", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 6, "End": 11, "Position": 2 } ] }, "wor": { "Term": "d29y", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 6, "End": 11, "Position": 2 } ] }, "worl": { "Term": "d29ybA==", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 6, "End": 11, "Position": 2 } ] }, "world": { "Term": "d29ybGQ=", "Locations": [ { "Field": "name", "ArrayPositions": [], "Start": 6, "End": 11, "Position": 2 } ] } }, null ] } gocb-2.6.3/testdata/transaction_begin_work_response.json000066400000000000000000000005051441755043100236150ustar00rootroot00000000000000{ "requestID": "e36e0202-7f4f-4083-9b73-993459353544", "clientContextID": "62d29101-0c9f-400d-af2b-9bd44a557a7c", "signature": { "*": "*" }, "results": [ ], "status": "success", "metrics": { "elapsedTime": "9.64915ms", "executionTime": "9.58744ms", "resultCount": 0, "resultSize": 0 } } gocb-2.6.3/testdata/transaction_gocbcore_cause_error.json000066400000000000000000000010251441755043100237230ustar00rootroot00000000000000{ "requestID": "9605e383-3da3-440e-a4e1-47d4b673401f", "clientContextID": "d4c97655-2e89-41ed-a46c-9f4e2a1eae5a", "signature": { "*": "*" }, "results": [], "errors": [ { "code": 19000, "msg": "Some pretend error", "cause": { "rollback": true, "retry": false, "raise": "expired" } } ], "status": "errors", "metrics": { "elapsedTime": "1.167435ms", "executionTime": "1.117429ms", "resultCount": 0, "resultSize": 0, "errorCount": 1 } } gocb-2.6.3/testdata/travel-sample-index-stored.json000066400000000000000000000016551441755043100223320ustar00rootroot00000000000000{ "name": "travel-sample-index-stored", "type": "fulltext-index", "params": { "doc_config": { "mode": "type_field", "type_field": "type" }, "mapping": { "analysis": { "analyzers": { "letterAnalyzer": { "tokenizer": "letter", "type": "custom" } } }, "default_analyzer": "standard", "default_datetime_parser": "dateTimeOptional", "default_field": "_all", "default_mapping": { "dynamic": true, "enabled": true }, "default_type": "_default", "index_dynamic": true, "store_dynamic": true, "type_field": "type" }, "store": { "kvStoreName": "mossStore" } }, "sourceType": "couchbase", "sourceName": "travel-sample", "sourceUUID": "", "sourceParams": {}, "planParams": { "maxPartitionsPerPIndex": 171, "numReplicas": 0 }, "uuid": "" } gocb-2.6.3/testdata/views_response_70.json000066400000000000000000000042571441755043100205350ustar00rootroot00000000000000{ "rows": [ { "doc": { "meta": { "id": "_design/aaa", "rev": "1-5d93a19b" }, "json": { "views": { "aaa": { "map": "function (doc, meta) {\n emit(meta.id, null);\n}" } } } }, "controllers": { "compact": "/pools/default/buckets/default/ddocs/_design%2Faaa/controller/compactView", "setUpdateMinChanges": "/pools/default/buckets/default/ddocs/_design%2Faaa/controller/setUpdateMinChanges" } }, { "doc": { "meta": { "id": "_design/dev_aaa", "rev": "1-53f6e7e2" }, "json": { "views": { "aaa": { "map": "function (doc, meta) {\n emit(meta.id, null);\n}" } } } }, "controllers": { "compact": "/pools/default/buckets/default/ddocs/_design%2Fdev_aaa/controller/compactView", "setUpdateMinChanges": "/pools/default/buckets/default/ddocs/_design%2Fdev_aaa/controller/setUpdateMinChanges" } }, { "doc": { "meta": { "id": "_design/dev_test", "rev": "1-19f7cc03" }, "json": { "views": { "test": { "map": "function (doc, meta) {\n emit(meta.id, null);\n}" } } } }, "controllers": { "compact": "/pools/default/buckets/default/ddocs/_design%2Fdev_test/controller/compactView", "setUpdateMinChanges": "/pools/default/buckets/default/ddocs/_design%2Fdev_test/controller/setUpdateMinChanges" } }, { "doc": { "meta": { "id": "_design/dev_test12", "rev": "1-3df87cb6" }, "json": { "views": { "test12": { "map": "function (doc, meta) {\n emit(meta.id, null);\n}" } } } }, "controllers": { "compact": "/pools/default/buckets/default/ddocs/_design%2Fdev_test12/controller/compactView", "setUpdateMinChanges": "/pools/default/buckets/default/ddocs/_design%2Fdev_test12/controller/setUpdateMinChanges" } } ] } gocb-2.6.3/testmain_test.go000066400000000000000000000176001441755043100156560ustar00rootroot00000000000000package gocb import ( "crypto/x509" "flag" "io/ioutil" "log" "net/http" "os" "runtime" "runtime/pprof" "strings" "testing" "time" cavescli "github.com/couchbaselabs/gocaves/client" "github.com/google/uuid" ) var globalConfig testConfig var globalBucket *Bucket var globalCollection *Collection var globalScope *Scope var globalCluster *testCluster var globalTracer *testTracer var globalMeter *testMeter type testConfig struct { Server string User string Password string Bucket string Version string Collection string Scope string FeatureFlags []TestFeatureFlag connstr string certsPath string auth Authenticator } func TestMain(m *testing.M) { initialGoroutineCount := runtime.NumGoroutine() server := envFlagString("GOCBSERVER", "server", "", "The connection string to connect to for a real server") user := envFlagString("GOCBUSER", "user", "", "The username to use to authenticate when using a real server") password := envFlagString("GOCBPASS", "pass", "", "The password to use to authenticate when using a real server") bucketName := envFlagString("GOCBBUCKET", "bucket", "default", "The bucket to use to test against") version := envFlagString("GOCBVER", "version", "", "The server version being tested against (major.minor.patch.build_edition)") collectionName := envFlagString("GOCBCOLL", "collection-name", "", "The name of the collection to use") scopeName := envFlagString("GOCBSCOP", "scope-name", "", "The name of the scope to use") featuresToTest := envFlagString("GOCBFEAT", "features", "", "The features that should be tested, applicable only for integration test runs") disableLogger := envFlagBool("GOCBNOLOG", "disable-logger", false, "Whether to disable the logger") certsPath := envFlagString("GOCBCERTS", "certs-path", "", "The path to the couchbase certs directory") enableTxnLoadTests := envFlagBool("GOCBENABLETXNLOAD", "enable-txn-load-tests", false, "Whether to enable transaction load tests") flag.Parse() if testing.Short() { mustBeNil := func(val interface{}, name string) { flag.Visit(func(f *flag.Flag) { if f.Name == name { panic(name + " cannot be used in short mode") } }) } mustBeNil(server, "server") mustBeNil(user, "user") mustBeNil(password, "pass") mustBeNil(bucketName, "bucket") mustBeNil(version, "version") mustBeNil(collectionName, "collection-name") mustBeNil(scopeName, "scope-name") mustBeNil(scopeName, "enable-txn-load-tests") } var featureFlags []TestFeatureFlag featureFlagStrs := strings.Split(*featuresToTest, ",") for _, featureFlagStr := range featureFlagStrs { if len(featureFlagStr) == 0 { continue } if featureFlagStr[0] == '+' { featureFlags = append(featureFlags, TestFeatureFlag{ Enabled: true, Feature: FeatureCode(featureFlagStr[1:]), }) continue } else if featureFlagStr[0] == '-' { featureFlags = append(featureFlags, TestFeatureFlag{ Enabled: false, Feature: FeatureCode(featureFlagStr[1:]), }) continue } panic("failed to parse specified feature codes") } // These are big tests, don't run unless explicitly enabled. if !(*enableTxnLoadTests) { featureFlags = append(featureFlags, TestFeatureFlag{ Enabled: false, Feature: TransactionsBulkFeature, }) } if !*disableLogger { SetLogger(VerboseStdioLogger()) } globalConfig.Server = *server globalConfig.User = *user globalConfig.Password = *password globalConfig.Bucket = *bucketName globalConfig.Version = *version globalConfig.Collection = *collectionName globalConfig.Scope = *scopeName globalConfig.FeatureFlags = featureFlags globalConfig.certsPath = *certsPath if !testing.Short() { setupCluster() } result := m.Run() if globalCluster != nil { err := globalCluster.Close(nil) if err != nil { panic(err) } if globalCluster.Mock != nil { err := globalCluster.Mock.Shutdown() if err != nil { panic(err) } } } // Loop for at most a second checking for goroutines leaks, this gives any HTTP goroutines time to shutdown start := time.Now() var finalGoroutineCount int for time.Now().Sub(start) <= 1*time.Second { runtime.Gosched() finalGoroutineCount = runtime.NumGoroutine() if finalGoroutineCount == initialGoroutineCount { break } time.Sleep(10 * time.Millisecond) } if finalGoroutineCount != initialGoroutineCount { log.Printf("Detected a goroutine leak (%d before != %d after), failing", initialGoroutineCount, finalGoroutineCount) pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) result = 1 } else { log.Printf("No goroutines appear to have leaked (%d before == %d after)", initialGoroutineCount, finalGoroutineCount) } os.Exit(result) } func envFlagString(envName, name, value, usage string) *string { envValue := os.Getenv(envName) if envValue != "" { value = envValue } return flag.String(name, value, usage) } func envFlagBool(envName, name string, value bool, usage string) *bool { envValue := os.Getenv(envName) if envValue != "" { if envValue == "0" { value = false } else if strings.ToLower(envValue) == "false" { value = false } else { value = true } } return flag.Bool(name, value, usage) } func setupCluster() { var err error var connStr string var mock *cavescli.Client var mockID string if globalConfig.Server == "" { if globalConfig.Version != "" { panic("version cannot be specified with mock") } mock, err = cavescli.NewClient(cavescli.NewClientOptions{ Version: "v0.0.1-69", }) if err != nil { panic(err.Error()) } mockID = uuid.New().String() connStr, err = mock.StartTesting(mockID, "gocb-"+Version()) if err != nil { panic(err) } globalConfig.Bucket = "default" globalConfig.Version = "0.0.1-53" globalConfig.Server = connStr globalConfig.User = "Administrator" globalConfig.Password = "password" // gocb itself doesn't use the default client but the mock downloader does so let's make sure that it // doesn't hold any goroutines open which will affect our goroutine leak detector. http.DefaultClient.CloseIdleConnections() } else { connStr = globalConfig.Server if globalConfig.Version == "" { globalConfig.Version = defaultServerVersion } } auth := PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, } options := ClusterOptions{Authenticator: auth} if globalConfig.certsPath != "" { rootCAs := x509.NewCertPool() files, err := ioutil.ReadDir(globalConfig.certsPath) if err != nil { log.Fatal(err) } for _, f := range files { certs, err := ioutil.ReadFile(globalConfig.certsPath + "/" + f.Name()) if strings.Contains(f.Name(), "roots") { if err != nil { log.Fatalf("Failed to append %q to RootCAs: %v", f, err) } if ok := rootCAs.AppendCertsFromPEM(certs); !ok { log.Println("No certs appended, using system certs only") } } } options.SecurityConfig.TLSRootCAs = rootCAs connStr = "couchbases://" + connStr } globalTracer = newTestTracer() globalMeter = newTestMeter() options.Tracer = globalTracer options.Meter = globalMeter if mock != nil { options.CompressionConfig.Disabled = true } cluster, err := Connect(connStr, options) if err != nil { panic(err.Error()) } globalConfig.connstr = connStr globalConfig.auth = auth nodeVersion, err := newNodeVersion(globalConfig.Version, mock != nil) if err != nil { panic(err.Error()) } globalCluster = &testCluster{ Cluster: cluster, Mock: mock, RunID: mockID, Version: nodeVersion, FeatureFlags: globalConfig.FeatureFlags, } globalBucket = globalCluster.Bucket(globalConfig.Bucket) if globalConfig.Scope != "" { globalScope = globalBucket.Scope(globalConfig.Scope) } else { globalScope = globalBucket.DefaultScope() } if globalConfig.Collection != "" { globalCollection = globalScope.Collection(globalConfig.Collection) } else { globalCollection = globalScope.Collection("_default") } } gocb-2.6.3/testsuite_test.go000066400000000000000000000261311441755043100160620ustar00rootroot00000000000000package gocb import ( "encoding/json" "fmt" "testing" "time" "github.com/stretchr/testify/suite" ) const ( defaultServerVersion = "5.1.0" ) var transactionsTestKeys map[string][]string type IntegrationTestSuite struct { suite.Suite } func (suite *IntegrationTestSuite) BeforeTest(suiteName, testName string) { globalTracer.Reset() globalMeter.Reset() if globalCluster.SupportsFeature(TransactionsBulkFeature) { suite.generateTransactionsKeys() } } func (suite *IntegrationTestSuite) SetupSuite() { if globalCluster.SupportsFeature(ReplicasFeature) { if globalCluster.SupportsFeature(DurabilityFeature) { suite.ensureReplicasUpEnhDura() } else { suite.ensureReplicasUpLegacyDura() } } } func (suite *IntegrationTestSuite) generateTransactionsKeys() { transactionsTestKeys = make(map[string][]string) k00tok19 := make([]string, 20) for i := range k00tok19 { k00tok19[i] = fmt.Sprintf("k%02d", i) } k19tok00 := make([]string, 20) for i := range k00tok19 { k19tok00[i] = fmt.Sprintf("k%02d", 49-i) } k20tok39 := make([]string, 20) for i := range k20tok39 { k20tok39[i] = fmt.Sprintf("k%02d", 50+i) } k000tok099 := make([]string, 100) for i := range k000tok099 { k000tok099[i] = fmt.Sprintf("k%02d", i) } k100tok199 := make([]string, 100) for i := range k100tok199 { k100tok199[i] = fmt.Sprintf("k%02d", 100+i) } k000tok499 := make([]string, 500) for i := range k000tok499 { k000tok499[i] = fmt.Sprintf("k%02d", i) } kCON := []string{"kCON"} kCandk00tok19 := append(append([]string{}, kCON...), k00tok19...) k00tok19andkC := append(append([]string{}, k00tok19...), kCON...) kCandk20tok39 := append(append([]string{}, kCON...), k20tok39...) k20tok39andkC := append(append([]string{}, k20tok39...), kCON...) kCandk000tok099 := append(append([]string{}, kCON...), k000tok099...) k000tok099andkC := append(append([]string{}, k000tok099...), kCON...) kCandk100tok199 := append(append([]string{}, kCON...), k100tok199...) k100tok199andkC := append(append([]string{}, k100tok199...), kCON...) transactionsTestKeys["k00tok19"] = k00tok19 transactionsTestKeys["k19tok00"] = k19tok00 transactionsTestKeys["k20tok39"] = k20tok39 transactionsTestKeys["k000tok099"] = k000tok099 transactionsTestKeys["k100tok199"] = k100tok199 transactionsTestKeys["k000tok499"] = k000tok499 transactionsTestKeys["kCON"] = kCON transactionsTestKeys["kCandk00tok19"] = kCandk00tok19 transactionsTestKeys["k00tok19andkC"] = k00tok19andkC transactionsTestKeys["kCandk20tok39"] = kCandk20tok39 transactionsTestKeys["k20tok39andkC"] = k20tok39andkC transactionsTestKeys["kCandk000tok099"] = kCandk000tok099 transactionsTestKeys["k000tok099andkC"] = k000tok099andkC transactionsTestKeys["kCandk100tok199"] = kCandk100tok199 transactionsTestKeys["k100tok199andkC"] = k100tok199andkC } func (suite *IntegrationTestSuite) AssertKVMetrics(metricName, op string, length int, atLeastLen bool) { suite.AssertMetrics(makeMetricsKeyFromCmd(metricName, "kv", op), length, atLeastLen) } func makeMetricsKeyFromCmd(metricName, service, op string) string { return makeMetricsKey(metricName, service, op) } func makeMetricsKey(metricName, service, op string) string { key := metricName + ":" + service if op != "" { key = key + ":" + op } return key } func (suite *IntegrationTestSuite) AssertMetrics(key string, length int, atLeastLen bool) { globalMeter.lock.Lock() defer globalMeter.lock.Unlock() recorders := globalMeter.recorders if suite.Assert().Contains(recorders, key) { if atLeastLen { suite.Assert().GreaterOrEqual(len(recorders[key].values), length) } else { suite.Assert().Len(recorders[key].values, length) } for _, val := range recorders[key].values { suite.Assert().NotZero(val) } } } func (suite *IntegrationTestSuite) ensureReplicasUpEnhDura() { success := suite.tryUntil(time.Now().Add(30*time.Second), 50*time.Millisecond, func() bool { _, err := globalCollection.Upsert("ensurereplicasup", "test", &UpsertOptions{ DurabilityLevel: DurabilityLevelPersistToMajority, }) if err != nil { time.Sleep(50 * time.Millisecond) return false } _, err = globalCollection.GetAnyReplica("ensurereplicasup", &GetAnyReplicaOptions{}) if err != nil { time.Sleep(50 * time.Millisecond) return false } return true }) if !success { panic("Ensuring that replicas are up did not succeed in time") } } func (suite *IntegrationTestSuite) ensureReplicasUpLegacyDura() { success := suite.tryUntil(time.Now().Add(30*time.Second), 50*time.Millisecond, func() bool { _, err := globalCollection.Upsert("ensurereplicasup", "test", &UpsertOptions{ PersistTo: 1, }) if err != nil { time.Sleep(50 * time.Millisecond) return false } _, err = globalCollection.GetAnyReplica("ensurereplicasup", &GetAnyReplicaOptions{}) if err != nil { time.Sleep(50 * time.Millisecond) return false } return true }) if !success { panic("Ensuring that replicas are up did not succeed in time") } } func (suite *IntegrationTestSuite) createBreweryDataset(datasetName, service, scope, collection string) (int, error) { var dataset []testBreweryDocument err := loadJSONTestDataset(datasetName, &dataset) if err != nil { return 0, err } if scope == "" { scope = "_default" } if collection == "" { collection = "_default" } scp := globalBucket.Scope(scope) col := scp.Collection(collection) for i, doc := range dataset { doc.Service = service _, err := col.Upsert(fmt.Sprintf("%s%d", service, i), doc, nil) if err != nil { return 0, err } } return len(dataset), nil } func (suite *IntegrationTestSuite) tryUntil(deadline time.Time, interval time.Duration, fn func() bool) bool { for { success := fn() if success { return true } sleepDeadline := time.Now().Add(interval) if sleepDeadline.After(deadline) { return false } time.Sleep(sleepDeadline.Sub(time.Now())) } } func (suite *IntegrationTestSuite) tryTimes(times int, interval time.Duration, fn func() bool) bool { for i := 0; i < times; i++ { success := fn() if success { return true } sleepDeadline := time.Now().Add(interval) time.Sleep(sleepDeadline.Sub(time.Now())) } return false } func (suite *IntegrationTestSuite) skipIfUnsupported(code FeatureCode) { if globalCluster.NotSupportsFeature(code) { suite.T().Skipf("Skipping test because feature %s unsupported or disabled", code) } } func (suite *IntegrationTestSuite) skipIfServerVersionEquals(version NodeVersion) { if globalCluster.Version.Equal(version) { suite.T().Skipf("Skipping test because it is not compatible with server version") } } func (suite *IntegrationTestSuite) dropAllIndexes() { mgr := globalCluster.QueryIndexes() var indexes []QueryIndex // Due to various eventual consistencies issues around dropping scopes query can sometimes // return us errors here. success := suite.tryUntil(time.Now().Add(30*time.Second), 100*time.Millisecond, func() bool { var err error indexes, err = mgr.GetAllIndexes(globalBucket.Name(), nil) if err != nil { suite.T().Logf("Failed to get all indexes: %v", err) return false } return true }) suite.Require().True(success, "Failed to get all indexes in time") for _, index := range indexes { if index.IsPrimary { mgr.DropPrimaryIndex(globalBucket.Name(), &DropPrimaryQueryIndexOptions{ CollectionName: index.CollectionName, ScopeName: index.ScopeName, }) } else { mgr.DropIndex(globalBucket.Name(), index.Name, &DropQueryIndexOptions{ CollectionName: index.CollectionName, ScopeName: index.ScopeName, }) } } globalMeter.Reset() globalTracer.Reset() } func (suite *IntegrationTestSuite) dropAllIndexesAtCollectionLevel() { mgr := globalCollection.QueryIndexes() var indexes []QueryIndex // Due to various eventual consistencies issues around dropping scopes query can sometimes // return us errors here. success := suite.tryUntil(time.Now().Add(30*time.Second), 100*time.Millisecond, func() bool { var err error indexes, err = mgr.GetAllIndexes(nil) if err != nil { suite.T().Logf("Failed to get all indexes: %v", err) return false } return true }) suite.Require().True(success, "Failed to get all indexes in time") for _, index := range indexes { if index.IsPrimary { mgr.DropPrimaryIndex(nil) } else { mgr.DropIndex(index.Name, nil) } } globalMeter.Reset() globalTracer.Reset() } type UnitTestSuite struct { suite.Suite } func TestIntegration(t *testing.T) { if testing.Short() { return } suite.Run(t, new(IntegrationTestSuite)) } func TestUnit(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (suite *UnitTestSuite) defaultTimeoutConfig() TimeoutsConfig { return TimeoutsConfig{ KVTimeout: 1000 * time.Second, KVDurableTimeout: 1000 * time.Second, AnalyticsTimeout: 1000 * time.Second, QueryTimeout: 1000 * time.Second, SearchTimeout: 1000 * time.Second, ManagementTimeout: 1000 * time.Second, ViewTimeout: 1000 * time.Second, } } func (suite *UnitTestSuite) bucket(name string, timeouts TimeoutsConfig, cli *mockConnectionManager) *Bucket { b := &Bucket{ bucketName: name, timeoutsConfig: TimeoutsConfig{ KVTimeout: timeouts.KVTimeout, KVDurableTimeout: timeouts.KVDurableTimeout, AnalyticsTimeout: timeouts.AnalyticsTimeout, QueryTimeout: timeouts.QueryTimeout, SearchTimeout: timeouts.SearchTimeout, ManagementTimeout: timeouts.ManagementTimeout, ViewTimeout: timeouts.ViewTimeout, }, transcoder: NewJSONTranscoder(), retryStrategyWrapper: newRetryStrategyWrapper(NewBestEffortRetryStrategy(nil)), tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, useServerDurations: true, useMutationTokens: true, connectionManager: cli, } return b } func (suite *UnitTestSuite) newCluster(cli connectionManager) *Cluster { cluster := clusterFromOptions(ClusterOptions{ Tracer: &NoopTracer{}, Meter: &NoopMeter{}, }) cluster.connectionManager = cli return cluster } func (suite *UnitTestSuite) newScope(b *Bucket, name string) *Scope { return newScope(b, name) } func (suite *UnitTestSuite) mustConvertToBytes(val interface{}) []byte { b, err := json.Marshal(val) suite.Require().Nil(err) return b } func (suite *UnitTestSuite) kvProvider(provider kvProvider, err error) func() (kvProvider, error) { return func() (kvProvider, error) { return provider, err } } func (suite *UnitTestSuite) kvCapabilityProvider(provider kvCapabilityVerifier, err error) func() (kvCapabilityVerifier, error) { return func() (kvCapabilityVerifier, error) { return provider, err } } func (suite *UnitTestSuite) collection(bucket, scope, collection string, provider kvProvider) *Collection { return &Collection{ bucket: &Bucket{bucketName: bucket}, collectionName: collection, scope: scope, getKvProvider: suite.kvProvider(provider, nil), timeoutsConfig: TimeoutsConfig{ KVTimeout: 2500 * time.Millisecond, }, transcoder: NewJSONTranscoder(), tracer: &NoopTracer{}, meter: &meterWrapper{meter: &NoopMeter{}}, retryStrategyWrapper: newRetryStrategyWrapper(NewBestEffortRetryStrategy(nil)), } } gocb-2.6.3/thresholdlogtracer.go000066400000000000000000000326101441755043100166700ustar00rootroot00000000000000package gocb import ( "encoding/json" "sort" "sync" "sync/atomic" "time" ) type thresholdLogGroup struct { name string floor time.Duration ops []*thresholdLogSpan lock sync.RWMutex } func initThresholdLogGroup(name string, floor time.Duration, size uint32) *thresholdLogGroup { return &thresholdLogGroup{ name: name, floor: floor, ops: make([]*thresholdLogSpan, 0, size), } } func (g *thresholdLogGroup) recordOp(span *thresholdLogSpan) { if span.duration < g.floor { return } // Preemptively check that we actually need to be inserted using a read lock first // this is a performance improvement measure to avoid locking the mutex all the time. g.lock.RLock() if len(g.ops) == cap(g.ops) && span.duration < g.ops[0].duration { // we are at capacity and we are faster than the fastest slow op g.lock.RUnlock() return } g.lock.RUnlock() g.lock.Lock() if len(g.ops) == cap(g.ops) && span.duration < g.ops[0].duration { // we are at capacity and we are faster than the fastest slow op g.lock.Unlock() return } l := len(g.ops) i := sort.Search(l, func(i int) bool { return span.duration < g.ops[i].duration }) // i represents the slot where it should be inserted if len(g.ops) < cap(g.ops) { if i == l { g.ops = append(g.ops, span) } else { g.ops = append(g.ops, nil) copy(g.ops[i+1:], g.ops[i:]) g.ops[i] = span } } else { if i == 0 { g.ops[i] = span } else { copy(g.ops[0:i-1], g.ops[1:i]) g.ops[i-1] = span } } g.lock.Unlock() } type thresholdLogItem struct { OperationName string `json:"operation_name,omitempty"` TotalTimeUs uint64 `json:"total_duration_us,omitempty"` EncodeDurationUs uint64 `json:"encode_duration_us,omitempty"` DispatchDurationUs uint64 `json:"total_dispatch_duration_us,omitempty"` ServerDurationUs uint64 `json:"total_server_duration_us,omitempty"` LastRemoteAddress string `json:"last_remote_socket,omitempty"` LastLocalAddress string `json:"last_local_socket,omitempty"` LastDispatchDurationUs uint64 `json:"last_dispatch_duration_us,omitempty"` LastServerDurationUs uint64 `json:"last_server_duration_us,omitempty"` LastOperationID string `json:"operation_id,omitempty"` LastLocalID string `json:"last_local_id,omitempty"` } type thresholdLogEntry struct { Count uint64 `json:"total_count"` Top []thresholdLogItem `json:"top_requests"` } type thresholdLogService map[string]thresholdLogEntry // ThresholdLoggingOptions is the set of options available for configuring threshold logging. type ThresholdLoggingOptions struct { Interval time.Duration SampleSize uint32 KVThreshold time.Duration KVScanThreshold time.Duration ViewsThreshold time.Duration QueryThreshold time.Duration SearchThreshold time.Duration AnalyticsThreshold time.Duration ManagementThreshold time.Duration } // ThresholdLoggingTracer is a specialized Tracer implementation which will automatically // log operations which fall outside of a set of thresholds. Note that this tracer is // only safe for use within the Couchbase SDK, uses by external event sources are // likely to fail. type ThresholdLoggingTracer struct { Interval time.Duration SampleSize uint32 KVThreshold time.Duration KVScanThreshold time.Duration ViewsThreshold time.Duration QueryThreshold time.Duration SearchThreshold time.Duration AnalyticsThreshold time.Duration ManagementThreshold time.Duration killCh chan struct{} refCount int32 nextTick time.Time groups map[string]*thresholdLogGroup } func NewThresholdLoggingTracer(opts *ThresholdLoggingOptions) *ThresholdLoggingTracer { if opts == nil { opts = &ThresholdLoggingOptions{} } if opts.Interval == 0 { opts.Interval = 10 * time.Second } if opts.SampleSize == 0 { opts.SampleSize = 10 } if opts.KVThreshold == 0 { opts.KVThreshold = 500 * time.Millisecond } if opts.KVScanThreshold == 0 { opts.KVScanThreshold = 1 * time.Second } if opts.ViewsThreshold == 0 { opts.ViewsThreshold = 1 * time.Second } if opts.QueryThreshold == 0 { opts.QueryThreshold = 1 * time.Second } if opts.SearchThreshold == 0 { opts.SearchThreshold = 1 * time.Second } if opts.AnalyticsThreshold == 0 { opts.AnalyticsThreshold = 1 * time.Second } if opts.ManagementThreshold == 0 { opts.ManagementThreshold = 1 * time.Second } t := &ThresholdLoggingTracer{ Interval: opts.Interval, SampleSize: opts.SampleSize, KVThreshold: opts.KVThreshold, KVScanThreshold: opts.KVScanThreshold, ViewsThreshold: opts.ViewsThreshold, QueryThreshold: opts.QueryThreshold, SearchThreshold: opts.SearchThreshold, AnalyticsThreshold: opts.AnalyticsThreshold, ManagementThreshold: opts.ManagementThreshold, } t.groups = map[string]*thresholdLogGroup{ "kv": initThresholdLogGroup("kv", t.KVThreshold, t.SampleSize), "kv_scan": initThresholdLogGroup("kv_scan", t.KVScanThreshold, t.SampleSize), "views": initThresholdLogGroup("views", t.ViewsThreshold, t.SampleSize), "query": initThresholdLogGroup("query", t.QueryThreshold, t.SampleSize), "search": initThresholdLogGroup("search", t.SearchThreshold, t.SampleSize), "analytics": initThresholdLogGroup("analytics", t.AnalyticsThreshold, t.SampleSize), "management": initThresholdLogGroup("management", t.ManagementThreshold, t.SampleSize), } if t.killCh == nil { t.killCh = make(chan struct{}) } if t.nextTick.IsZero() { t.nextTick = time.Now().Add(t.Interval) } return t } // AddRef is used internally to keep track of the number of Cluster instances referring to it. // This is used to correctly shut down the aggregation routines once there are no longer any // instances tracing to it. func (t *ThresholdLoggingTracer) AddRef() int32 { newRefCount := atomic.AddInt32(&t.refCount, 1) if newRefCount == 1 { t.startLoggerRoutine() } return newRefCount } // DecRef is the counterpart to AddRef (see AddRef for more information). func (t *ThresholdLoggingTracer) DecRef() int32 { newRefCount := atomic.AddInt32(&t.refCount, -1) if newRefCount == 0 { t.killCh <- struct{}{} } return newRefCount } func (t *ThresholdLoggingTracer) buildJSONData() thresholdLogService { // Preallocate space to copy the ops into... oldOps := make([]*thresholdLogSpan, t.SampleSize) jsonData := make(thresholdLogService) for _, g := range t.groups { g.lock.Lock() // Escape early if we have no ops to log... if len(g.ops) == 0 { g.lock.Unlock() continue } // Copy out our ops so we can cheaply print them out without blocking // our ops from actually being recorded in other goroutines (which would // effectively slow down the op pipeline for logging). oldOps = oldOps[0:len(g.ops)] copy(oldOps, g.ops) g.ops = g.ops[:0] g.lock.Unlock() entry := thresholdLogEntry{} for i := len(oldOps) - 1; i >= 0; i-- { op := oldOps[i] localAddr := op.lastDispatchLocal if localAddr != "" && op.lastDispatchLocalPort != "" { localAddr = localAddr + ":" + op.lastDispatchLocalPort } peerAddr := op.lastDispatchPeer if peerAddr != "" && op.lastDispatchPeerPort != "" { peerAddr = peerAddr + ":" + op.lastDispatchPeerPort } entry.Top = append(entry.Top, thresholdLogItem{ OperationName: op.opName, TotalTimeUs: uint64(op.duration / time.Microsecond), DispatchDurationUs: uint64(op.totalDispatchDuration / time.Microsecond), ServerDurationUs: uint64(op.totalServerDuration / time.Microsecond), EncodeDurationUs: uint64(op.totalEncodeDuration / time.Microsecond), LastLocalAddress: localAddr, LastRemoteAddress: peerAddr, LastDispatchDurationUs: uint64(op.lastDispatchDuration / time.Microsecond), LastServerDurationUs: uint64(op.lastServerDuration / time.Microsecond), LastOperationID: op.lastOperationID, LastLocalID: op.lastLocalID, }) } entry.Count = uint64(len(entry.Top)) jsonData[g.name] = entry } return jsonData } func (t *ThresholdLoggingTracer) logRecordedRecords() { jsonData := t.buildJSONData() if len(jsonData) == 0 { // Nothing to log so make sure we don't just log empty objects. return } jsonBytes, err := json.Marshal(jsonData) if err != nil { logDebugf("Failed to generate threshold logging service JSON: %s", err) } logInfof("Threshold Log: %s", jsonBytes) } func (t *ThresholdLoggingTracer) startLoggerRoutine() { go t.loggerRoutine() } func (t *ThresholdLoggingTracer) loggerRoutine() { for { select { case <-time.After(time.Until(t.nextTick)): t.nextTick = t.nextTick.Add(t.Interval) t.logRecordedRecords() case <-t.killCh: t.logRecordedRecords() return } } } func (t *ThresholdLoggingTracer) recordOp(span *thresholdLogSpan) { switch span.serviceName { case "mgmt": t.groups["management"].recordOp(span) case "kv": t.groups["kv"].recordOp(span) case "kv_scan": t.groups["kv_scan"].recordOp(span) case "views": t.groups["views"].recordOp(span) case "query": t.groups["query"].recordOp(span) case "search": t.groups["search"].recordOp(span) case "analytics": t.groups["analytics"].recordOp(span) } } // RequestSpan belongs to the Tracer interface. func (t *ThresholdLoggingTracer) RequestSpan(parentContext RequestSpanContext, operationName string) RequestSpan { span := &thresholdLogSpan{ tracer: t, opName: operationName, startTime: time.Now(), } if context, ok := parentContext.(*thresholdLogSpanContext); ok { span.parent = context.span } return span } type thresholdLogSpan struct { tracer *ThresholdLoggingTracer parent *thresholdLogSpan opName string startTime time.Time serviceName string peerAddress string localAddress string peerPort string localPort string serverDuration time.Duration duration time.Duration totalServerDuration time.Duration totalDispatchDuration time.Duration totalEncodeDuration time.Duration lastDispatchPeer string lastDispatchLocal string lastDispatchPeerPort string lastDispatchLocalPort string lastDispatchDuration time.Duration lastServerDuration time.Duration lastOperationID string lastLocalID string lock sync.Mutex } func (n *thresholdLogSpan) Context() RequestSpanContext { return &thresholdLogSpanContext{n} } func (n *thresholdLogSpan) SetAttribute(key string, value interface{}) { var ok bool switch key { case spanAttribServerDurationKey: if n.serverDuration, ok = value.(time.Duration); !ok { logDebugf("Failed to cast span db.couchbase.server_duration tag") } case spanAttribServiceKey: if n.serviceName, ok = value.(string); !ok { logDebugf("Failed to cast span db.couchbase.service tag") } case spanAttribNetPeerNameKey: if n.peerAddress, ok = value.(string); !ok { logDebugf("Failed to cast span net.peer.name tag") } case spanAttribNetHostNameKey: if n.localAddress, ok = value.(string); !ok { logDebugf("Failed to cast span net.host.local tag") } case spanAttribOperationIDKey: if n.lastOperationID, ok = value.(string); !ok { logDebugf("Failed to cast span db.couchbase.operation_id tag") } case spanAttribLocalIDKey: if n.lastLocalID, ok = value.(string); !ok { logDebugf("Failed to cast span db.couchbase.local_id tag") } case spanAttribNetPeerPortKey: if n.peerPort, ok = value.(string); !ok { logDebugf("Failed to cast span net.peer.port tag") } case spanAttribNetHostPortKey: if n.localPort, ok = value.(string); !ok { logDebugf("Failed to cast span net.host.port tag") } } } func (n *thresholdLogSpan) AddEvent(key string, timestamp time.Time) { } func (n *thresholdLogSpan) End() { n.duration = time.Since(n.startTime) n.totalServerDuration += n.serverDuration if n.opName == spanNameDispatchToServer { n.totalDispatchDuration += n.duration n.lastDispatchPeer = n.peerAddress n.lastDispatchLocal = n.localAddress n.lastDispatchPeerPort = n.peerPort n.lastDispatchLocalPort = n.localPort n.lastDispatchDuration = n.duration n.lastServerDuration = n.serverDuration } if n.opName == spanNameRequestEncoding { n.totalEncodeDuration += n.duration } if n.parent != nil { n.parent.lock.Lock() n.parent.totalServerDuration += n.totalServerDuration n.parent.totalDispatchDuration += n.totalDispatchDuration n.parent.totalEncodeDuration += n.totalEncodeDuration if n.lastDispatchPeer != "" || n.lastDispatchDuration > 0 { n.parent.lastDispatchPeer = n.lastDispatchPeer n.parent.lastDispatchDuration = n.lastDispatchDuration } if n.lastDispatchPeer != "" || n.lastServerDuration > 0 { n.parent.lastServerDuration = n.lastServerDuration } if n.lastDispatchLocal != "" { n.parent.lastDispatchLocal = n.lastDispatchLocal } if n.lastOperationID != "" { n.parent.lastOperationID = n.lastOperationID } if n.lastLocalID != "" { n.parent.lastLocalID = n.lastLocalID } if n.lastDispatchLocalPort != "" { n.parent.lastDispatchLocalPort = n.lastDispatchLocalPort } if n.lastDispatchPeerPort != "" { n.parent.lastDispatchPeerPort = n.lastDispatchPeerPort } if n.lastDispatchPeerPort != "" { n.parent.lastDispatchPeerPort = n.lastDispatchPeerPort } n.parent.lock.Unlock() } if n.serviceName != "" { n.tracer.recordOp(n) } } type thresholdLogSpanContext struct { span *thresholdLogSpan } gocb-2.6.3/thresholdlogtracer_test.go000066400000000000000000000114601441755043100177270ustar00rootroot00000000000000package gocb import ( "context" "time" ) func (suite *UnitTestSuite) TestThresholdGroup() { time.Sleep(100 * time.Millisecond) grp := initThresholdLogGroup("Test", 2*time.Millisecond, 3) grp.recordOp(&thresholdLogSpan{duration: 1 * time.Millisecond}) grp.recordOp(&thresholdLogSpan{duration: 2 * time.Millisecond}) if len(grp.ops) != 1 { suite.T().Fatalf("Failed to ignore duration below threshold") } grp.recordOp(&thresholdLogSpan{duration: 6 * time.Millisecond}) grp.recordOp(&thresholdLogSpan{duration: 4 * time.Millisecond}) grp.recordOp(&thresholdLogSpan{duration: 5 * time.Millisecond}) grp.recordOp(&thresholdLogSpan{duration: 2 * time.Millisecond}) grp.recordOp(&thresholdLogSpan{duration: 9 * time.Millisecond}) if len(grp.ops) != 3 { suite.T().Fatalf("Failed to reach real capacity") } if grp.ops[0].duration != 5*time.Millisecond { suite.T().Fatalf("Failed to insert in correct order (1)") } if grp.ops[1].duration != 6*time.Millisecond { suite.T().Fatalf("Failed to insert in correct order (2)") } if grp.ops[2].duration != 9*time.Millisecond { suite.T().Fatalf("Failed to insert in correct order (3)") } } func (suite *UnitTestSuite) TestThresholdLogger() { logger := NewThresholdLoggingTracer(&ThresholdLoggingOptions{ KVThreshold: 1, QueryThreshold: 1, }) span1 := logger.RequestSpan(context.Background(), "Set") span1.SetAttribute(spanAttribDBNameKey, "mybucket") span1.SetAttribute(spanAttribDBCollectionNameKey, "mycollection") span1.SetAttribute(spanAttribDBScopeNameKey, "myscope") span1.SetAttribute(spanAttribServiceKey, "kv") span1.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) span1Encode := logger.RequestSpan(span1.Context(), spanNameRequestEncoding) span1Encode.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) time.Sleep(50 * time.Microsecond) span1Encode.End() span1Dispatch := logger.RequestSpan(span1.Context(), spanNameDispatchToServer) span1Dispatch.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) span1Dispatch.SetAttribute(spanAttribOperationIDKey, "1") span1Dispatch.SetAttribute(spanAttribLocalIDKey, "12345") span1Dispatch.SetAttribute(spanAttribNetHostNameKey, "localhost") span1Dispatch.SetAttribute(spanAttribNetHostPortKey, "5431") span1Dispatch.SetAttribute(spanAttribNetPeerNameKey, "remotehost") span1Dispatch.SetAttribute(spanAttribNetPeerPortKey, "11210") span1Dispatch.SetAttribute(spanAttribServerDurationKey, 1100*time.Microsecond) time.Sleep(50 * time.Microsecond) span1Dispatch.End() span1.End() span2 := logger.RequestSpan(context.Background(), "query") span2.SetAttribute(spanAttribServiceKey, "query") span2.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) span2Encode := logger.RequestSpan(span2.Context(), spanNameRequestEncoding) span2Encode.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) time.Sleep(50 * time.Microsecond) span2Encode.End() span2Dispatch := logger.RequestSpan(span2.Context(), spanNameDispatchToServer) span2Dispatch.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) span2Dispatch.SetAttribute(spanAttribOperationIDKey, "clientcontextid") span2Dispatch.SetAttribute(spanAttribNetPeerNameKey, "remotehost") span2Dispatch.SetAttribute(spanAttribNetPeerPortKey, "8093") time.Sleep(50 * time.Microsecond) span2Dispatch.End() span2.End() data := logger.buildJSONData() suite.Assert().Len(data, 2) if suite.Assert().Contains(data, "kv") { kvData := data["kv"] suite.Assert().Equal(uint64(1), kvData.Count) top := kvData.Top suite.Assert().Equal(1, len(top)) item := top[0] suite.Assert().Equal("Set", item.OperationName) suite.Assert().Equal("localhost:5431", item.LastLocalAddress) suite.Assert().Equal(uint64(1100), item.LastServerDurationUs) suite.Assert().NotZero(item.DispatchDurationUs) suite.Assert().NotZero(item.EncodeDurationUs) suite.Assert().NotZero(item.LastDispatchDurationUs) suite.Assert().Equal("12345", item.LastLocalID) suite.Assert().Equal("1", item.LastOperationID) suite.Assert().Equal("remotehost:11210", item.LastRemoteAddress) suite.Assert().Equal(uint64(1100), item.ServerDurationUs) suite.Assert().NotZero(item.TotalTimeUs) } if suite.Assert().Contains(data, "query") { queryData := data["query"] suite.Assert().Equal(uint64(1), queryData.Count) top := queryData.Top suite.Assert().Equal(1, len(top)) item := top[0] suite.Assert().Equal("query", item.OperationName) suite.Assert().Empty(item.LastLocalAddress) suite.Assert().Empty(item.LastServerDurationUs) suite.Assert().NotZero(item.DispatchDurationUs) suite.Assert().NotZero(item.EncodeDurationUs) suite.Assert().NotZero(item.LastDispatchDurationUs) suite.Assert().Equal("clientcontextid", item.LastOperationID) suite.Assert().Equal("remotehost:8093", item.LastRemoteAddress) suite.Assert().NotZero(item.TotalTimeUs) } } gocb-2.6.3/token.go000066400000000000000000000112231441755043100141060ustar00rootroot00000000000000package gocb import ( "encoding/json" "fmt" "strconv" gocbcore "github.com/couchbase/gocbcore/v10" ) // MutationToken holds the mutation state information from an operation. type MutationToken struct { token gocbcore.MutationToken bucketName string } type bucketToken struct { SeqNo uint64 `json:"seqno"` VbUUID string `json:"vbuuid"` } // BucketName returns the name of the bucket that this token belongs to. func (mt MutationToken) BucketName() string { return mt.bucketName } // PartitionUUID returns the UUID of the vbucket that this token belongs to. func (mt MutationToken) PartitionUUID() uint64 { return uint64(mt.token.VbUUID) } // PartitionID returns the ID of the vbucket that this token belongs to. func (mt MutationToken) PartitionID() uint64 { return uint64(mt.token.VbID) } // SequenceNumber returns the sequence number of the vbucket that this token belongs to. func (mt MutationToken) SequenceNumber() uint64 { return uint64(mt.token.SeqNo) } func (mt bucketToken) MarshalJSON() ([]byte, error) { info := []interface{}{mt.SeqNo, mt.VbUUID} return json.Marshal(info) } func (mt *bucketToken) UnmarshalJSON(data []byte) error { info := []interface{}{&mt.SeqNo, &mt.VbUUID} return json.Unmarshal(data, &info) } type bucketTokens map[string]*bucketToken type mutationStateData map[string]*bucketTokens type searchMutationState map[string]map[string]uint64 // MutationState holds and aggregates MutationToken's across multiple operations. type MutationState struct { tokens []MutationToken } // NewMutationState creates a new MutationState for tracking mutation state. func NewMutationState(tokens ...MutationToken) *MutationState { mt := &MutationState{} mt.Add(tokens...) return mt } // Add includes an operation's mutation information in this mutation state. func (mt *MutationState) Add(tokens ...MutationToken) { for _, token := range tokens { if token.bucketName != "" { mt.tokens = append(mt.tokens, token) } } } // MutationStateInternal specifies internal operations. // Internal: This should never be used and is not supported. type MutationStateInternal struct { mt *MutationState } // Internal return a new MutationStateInternal. // Internal: This should never be used and is not supported. func (mt *MutationState) Internal() *MutationStateInternal { return &MutationStateInternal{ mt: mt, } } // Add includes an operation's mutation information in this mutation state. func (mti *MutationStateInternal) Add(bucket string, tokens ...gocbcore.MutationToken) { for _, token := range tokens { mti.mt.Add(MutationToken{ bucketName: bucket, token: token, }) } } // Tokens returns the tokens belonging to the mutation state. func (mti *MutationStateInternal) Tokens() []MutationToken { return mti.mt.tokens } // MarshalJSON marshal's this mutation state to JSON. func (mt *MutationState) MarshalJSON() ([]byte, error) { var data mutationStateData for _, token := range mt.tokens { if data == nil { data = make(mutationStateData) } bucketName := token.bucketName if (data)[bucketName] == nil { tokens := make(bucketTokens) (data)[bucketName] = &tokens } vbID := fmt.Sprintf("%d", token.token.VbID) stateToken := (*(data)[bucketName])[vbID] if stateToken == nil { stateToken = &bucketToken{} (*(data)[bucketName])[vbID] = stateToken } stateToken.SeqNo = uint64(token.token.SeqNo) stateToken.VbUUID = fmt.Sprintf("%d", token.token.VbUUID) } return json.Marshal(data) } // UnmarshalJSON unmarshal's a mutation state from JSON. func (mt *MutationState) UnmarshalJSON(data []byte) error { var stateData mutationStateData err := json.Unmarshal(data, &stateData) if err != nil { return err } for bucketName, bTokens := range stateData { for vbIDStr, stateToken := range *bTokens { vbID, err := strconv.Atoi(vbIDStr) if err != nil { return err } vbUUID, err := strconv.Atoi(stateToken.VbUUID) if err != nil { return err } token := MutationToken{ bucketName: bucketName, token: gocbcore.MutationToken{ VbID: uint16(vbID), VbUUID: gocbcore.VbUUID(vbUUID), SeqNo: gocbcore.SeqNo(stateToken.SeqNo), }, } mt.tokens = append(mt.tokens, token) } } return nil } // toSearchMutationState is specific to search, search doesn't accept tokens in the same format as other services. func (mt *MutationState) toSearchMutationState(indexName string) searchMutationState { data := make(searchMutationState) for _, token := range mt.tokens { _, ok := data[indexName] if !ok { data[indexName] = make(map[string]uint64) } data[indexName][fmt.Sprintf("%d/%d", token.token.VbID, token.token.VbUUID)] = uint64(token.token.SeqNo) } return data } gocb-2.6.3/token_test.go000066400000000000000000000050031441755043100151440ustar00rootroot00000000000000package gocb import ( "encoding/json" "strings" gocbcore "github.com/couchbase/gocbcore/v10" ) func (suite *UnitTestSuite) TestMutationState_Add() { fakeBucket := &Bucket{} fakeBucket.bucketName = "frank" fakeToken1 := MutationToken{ token: gocbcore.MutationToken{ VbID: 1, VbUUID: gocbcore.VbUUID(9), SeqNo: gocbcore.SeqNo(12), }, bucketName: fakeBucket.Name(), } fakeToken2 := MutationToken{ token: gocbcore.MutationToken{ VbID: 2, VbUUID: gocbcore.VbUUID(1), SeqNo: gocbcore.SeqNo(22), }, bucketName: fakeBucket.Name(), } fakeToken3 := MutationToken{ token: gocbcore.MutationToken{ VbID: 2, VbUUID: gocbcore.VbUUID(4), SeqNo: gocbcore.SeqNo(99), }, bucketName: fakeBucket.Name(), } state := NewMutationState(fakeToken1, fakeToken2) state.Add(fakeToken3) bytes, err := json.Marshal(&state) if err != nil { suite.T().Fatalf("Failed to marshal %v", err) } if strings.Compare(string(bytes), "{\"frank\":{\"1\":[12,\"9\"],\"2\":[99,\"4\"]}}") != 0 { suite.T().Fatalf("Failed to generate correct JSON output %s", bytes) } // So as to avoid testing on private properties we'll check if unmarshal works by marshaling the result. var afterState MutationState err = json.Unmarshal(bytes, &afterState) if err != nil { suite.T().Fatalf("Failed to unmarshal %v", err) } bytes, err = json.Marshal(&state) if err != nil { suite.T().Fatalf("Failed to marshal %v", err) } if strings.Compare(string(bytes), "{\"frank\":{\"1\":[12,\"9\"],\"2\":[99,\"4\"]}}") != 0 { suite.T().Fatalf("Failed to generate correct JSON output %s", bytes) } } func (suite *UnitTestSuite) TestMutationState_toSeachMutationState() { fakeBucket := &Bucket{} fakeBucket.bucketName = "frank" fakeToken1 := MutationToken{ token: gocbcore.MutationToken{ VbID: 1, VbUUID: gocbcore.VbUUID(9), SeqNo: gocbcore.SeqNo(12), }, bucketName: fakeBucket.Name(), } fakeToken2 := MutationToken{ token: gocbcore.MutationToken{ VbID: 2, VbUUID: gocbcore.VbUUID(1), SeqNo: gocbcore.SeqNo(22), }, bucketName: fakeBucket.Name(), } state := NewMutationState(fakeToken1, fakeToken2) searchToken := state.toSearchMutationState("frankindex") // What we actually care about is the format once marshaled. bytes, err := json.Marshal(&searchToken) if err != nil { suite.T().Fatalf("Failed to marshal %v", err) } if strings.Compare(string(bytes), "{\"frankindex\":{\"1/9\":12,\"2/1\":22}}") != 0 { suite.T().Fatalf("Failed to generate correct JSON output %s", bytes) } } gocb-2.6.3/tracing.go000066400000000000000000000057421441755043100144260ustar00rootroot00000000000000package gocb import ( "github.com/couchbase/gocbcore/v10" "time" ) func tracerAddRef(tracer RequestTracer) { if tracer == nil { return } if refTracer, ok := tracer.(interface { AddRef() int32 }); ok { refTracer.AddRef() } } func tracerDecRef(tracer RequestTracer) { if tracer == nil { return } if refTracer, ok := tracer.(interface { DecRef() int32 }); ok { refTracer.DecRef() } } // RequestTracer describes the tracing abstraction in the SDK. type RequestTracer interface { RequestSpan(parentContext RequestSpanContext, operationName string) RequestSpan } // RequestSpan is the interface for spans that are created by a RequestTracer. type RequestSpan interface { End() Context() RequestSpanContext AddEvent(name string, timestamp time.Time) SetAttribute(key string, value interface{}) } // RequestSpanContext is the interface for for external span contexts that can be passed in into the SDK option blocks. type RequestSpanContext interface { } type coreRequestTracerWrapper struct { tracer RequestTracer } func (tracer *coreRequestTracerWrapper) RequestSpan(parentContext gocbcore.RequestSpanContext, operationName string) gocbcore.RequestSpan { return &coreRequestSpanWrapper{ span: tracer.tracer.RequestSpan(parentContext, operationName), } } type coreRequestSpanWrapper struct { span RequestSpan } func (span *coreRequestSpanWrapper) End() { span.span.End() } func (span *coreRequestSpanWrapper) Context() gocbcore.RequestSpanContext { return span.span.Context() } func (span *coreRequestSpanWrapper) SetAttribute(key string, value interface{}) { span.span.SetAttribute(key, value) } func (span *coreRequestSpanWrapper) AddEvent(key string, timestamp time.Time) { span.span.SetAttribute(key, timestamp) } type noopSpan struct{} type noopSpanContext struct{} var ( defaultNoopSpanContext = noopSpanContext{} defaultNoopSpan = noopSpan{} ) // NoopTracer is a RequestTracer implementation that does not perform any tracing. type NoopTracer struct { // nolint: unused } // RequestSpan creates a new RequestSpan. func (tracer *NoopTracer) RequestSpan(parentContext RequestSpanContext, operationName string) RequestSpan { return defaultNoopSpan } // End completes the span. func (span noopSpan) End() { } // Context returns the RequestSpanContext for this span. func (span noopSpan) Context() RequestSpanContext { return defaultNoopSpanContext } // SetAttribute adds an attribute to this span. func (span noopSpan) SetAttribute(key string, value interface{}) { } // AddEvent adds an event to this span. func (span noopSpan) AddEvent(key string, timestamp time.Time) { } func createSpan(tracer RequestTracer, parent RequestSpan, operationType, service string) RequestSpan { var tracectx RequestSpanContext if parent != nil { tracectx = parent.Context() } span := tracer.RequestSpan(tracectx, operationType) span.SetAttribute(spanAttribDBSystemKey, spanAttribDBSystemValue) if service != "" { span.SetAttribute(spanAttribServiceKey, service) } return span } gocb-2.6.3/tracing_test.go000066400000000000000000000231641441755043100154630ustar00rootroot00000000000000package gocb import ( "sync" "time" ) type testSpan struct { Name string Tags map[string]interface{} Finished bool ParentContext RequestSpanContext Spans map[RequestSpanContext][]*testSpan } func (ts *testSpan) End() { ts.Finished = true } func (ts *testSpan) Context() RequestSpanContext { return ts.Spans } func newTestSpan(operationName string, parentContext RequestSpanContext) *testSpan { return &testSpan{ Name: operationName, Tags: make(map[string]interface{}), ParentContext: parentContext, Spans: make(map[RequestSpanContext][]*testSpan), } } func (ts *testSpan) SetAttribute(key string, value interface{}) { ts.Tags[key] = value } func (ts *testSpan) AddEvent(key string, timestamp time.Time) { } type testTracer struct { Spans map[RequestSpanContext][]*testSpan lock sync.Mutex } func newTestTracer() *testTracer { return &testTracer{ Spans: make(map[RequestSpanContext][]*testSpan), } } func (tt *testTracer) RequestSpan(parentContext RequestSpanContext, operationName string) RequestSpan { span := newTestSpan(operationName, parentContext) tt.lock.Lock() if parentContext == nil { tt.Spans[parentContext] = append(tt.Spans[parentContext], span) } else { ctx, ok := parentContext.(map[RequestSpanContext][]*testSpan) if ok { ctx[operationName] = append(ctx[operationName], span) } else { tt.Spans[parentContext] = append(tt.Spans[parentContext], span) } } tt.lock.Unlock() return span } func (tt *testTracer) Reset() { tt.lock.Lock() tt.Spans = make(map[RequestSpanContext][]*testSpan) tt.lock.Unlock() } func (tt *testTracer) GetSpans() map[RequestSpanContext][]*testSpan { spans := make(map[RequestSpanContext][]*testSpan) tt.lock.Lock() for ctx, ttSpans := range tt.Spans { // The underlying spans won't change but the list at the top level itself could do. thisSpans := make([]*testSpan, len(ttSpans)) for i, span := range ttSpans { thisSpans[i] = span } spans[ctx] = thisSpans } tt.lock.Unlock() return spans } func (suite *IntegrationTestSuite) AssertKvOpSpan(span *testSpan, opName, cmdName string, hasEncoding bool, durability DurabilityLevel) { suite.AssertKvSpan(span, opName, durability) if hasEncoding { suite.AssertEncodingSpansEq(span.Spans, 1) } suite.AssertCmdSpans(span.Spans, cmdName) } type HTTPOpSpanExpectations struct { bucket string scope string collection string service string operationID string numDispatchSpans int atLeastNumDispatchSpans bool hasEncoding bool dispatchOperationID string statement string } func (suite *IntegrationTestSuite) AssertHTTPOpSpan(span *testSpan, opName string, expectations HTTPOpSpanExpectations) { suite.AssertHTTPSpan(span, opName, expectations.bucket, expectations.scope, expectations.collection, expectations.service, expectations.operationID, expectations.statement) if expectations.hasEncoding { suite.AssertEncodingSpansEq(span.Spans, 1) } if expectations.atLeastNumDispatchSpans { suite.AssertHTTPDispatchSpansGE(span.Spans, expectations.numDispatchSpans, expectations.dispatchOperationID) } else { suite.AssertHTTPDispatchSpansEQ(span.Spans, expectations.numDispatchSpans, expectations.dispatchOperationID) } } func (suite *IntegrationTestSuite) RequireQueryMgmtOpSpan(span *testSpan, opName, childType string) *testSpan { suite.Assert().Equal(opName, span.Name) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().Equal("management", span.Tags["db.couchbase.service"]) suite.Require().Len(span.Spans, 1) suite.Require().Len(span.Spans[childType], 1) return span.Spans[childType][0] } func (suite *IntegrationTestSuite) AssertKvSpan(span *testSpan, expectedName string, durability DurabilityLevel) { scope := globalConfig.Scope if scope == "" { scope = "_default" } col := globalConfig.Collection if col == "" { col = "_default" } suite.Assert().Equal(expectedName, span.Name) numTags := 6 if durability > DurabilityLevelNone { numTags = 7 } suite.Assert().Equal(numTags, len(span.Tags)) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().Equal(globalConfig.Bucket, span.Tags["db.name"]) suite.Assert().Equal(scope, span.Tags["db.couchbase.scope"]) suite.Assert().Equal(col, span.Tags["db.couchbase.collection"]) suite.Assert().Equal("kv", span.Tags["db.couchbase.service"]) suite.Assert().Equal(expectedName, span.Tags["db.operation"]) if durability == DurabilityLevelNone { suite.Assert().NotContains(span.Tags, "db.couchbase.durability") } else { if duraLevel, err := durability.toManagementAPI(); err == nil { suite.Assert().Equal(duraLevel, span.Tags["db.couchbase.durability"]) } else { logDebugf("Failed to get durability level: %v", err) } } suite.Assert().True(span.Finished) } func (suite *IntegrationTestSuite) AssertHTTPSpan(span *testSpan, name, bucket, scope, collection, service, op, statement string) { suite.Assert().Equal(name, span.Name) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().Equal(service, span.Tags["db.couchbase.service"]) spans := 2 if op == "" { suite.Assert().NotContains(span.Tags, "db.operation") } else { spans++ suite.Assert().Equal(op, span.Tags["db.operation"]) } if bucket == "" { suite.Assert().NotContains(span.Tags, "db.name") } else { spans++ suite.Assert().Equal(globalConfig.Bucket, span.Tags["db.name"]) } if scope == "" { suite.Assert().NotContains(span.Tags, "db.couchbase.scope") } else { spans++ suite.Assert().Equal(scope, span.Tags["db.couchbase.scope"]) } if collection == "" { suite.Assert().NotContains(span.Tags, "db.couchbase.collection") } else { spans++ suite.Assert().Equal(collection, span.Tags["db.couchbase.collection"]) } if statement == "" { suite.Assert().NotContains(span.Tags, "db.statement") } else if statement == "any" { spans++ suite.Assert().NotEmpty(span.Tags["db.statement"]) } else { spans++ suite.Assert().Equal(statement, span.Tags["db.statement"]) } suite.Assert().Equal(spans, len(span.Tags)) suite.Assert().True(span.Finished) } func (suite *IntegrationTestSuite) AssertEncodingSpansEq(parents map[RequestSpanContext][]*testSpan, num int) { if suite.Assert().Contains(parents, "request_encoding") { spans := parents["request_encoding"] if suite.Assert().Equal(num, len(spans)) { for i := 0; i < num; i++ { suite.AssertEncodingSpan(spans[i]) } } } } func (suite *IntegrationTestSuite) AssertEncodingSpan(span *testSpan) { suite.Assert().Equal("request_encoding", span.Name) suite.Assert().Equal(1, len(span.Tags)) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().True(span.Finished) } func (suite *IntegrationTestSuite) AssertCmdSpans(parents map[RequestSpanContext][]*testSpan, cmdName string) { spans := parents[cmdName] for i := 0; i < len(spans); i++ { suite.AssertCmdSpan(spans[i], cmdName) } } func (suite *IntegrationTestSuite) AssertCmdSpan(span *testSpan, expectedName string) { suite.Assert().Equal(expectedName, span.Name) suite.Assert().Equal(2, len(span.Tags)) suite.Assert().True(span.Finished) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().Contains(span.Tags, "db.couchbase.retries") suite.AssertKVDispatchSpans(span.Spans) } func (suite *IntegrationTestSuite) AssertKVDispatchSpans(parents map[RequestSpanContext][]*testSpan) { spans := parents["dispatch_to_server"] for i := 0; i < len(spans); i++ { suite.AssertKVDispatchSpan(spans[i]) } } func (suite *IntegrationTestSuite) AssertHTTPDispatchSpansEQ(parents map[RequestSpanContext][]*testSpan, num int, operationID string) { spans := parents["dispatch_to_server"] if suite.Assert().Equal(num, len(spans)) { for i := 0; i < len(spans); i++ { suite.AssertHTTPDispatchSpan(spans[i], operationID) } } } func (suite *IntegrationTestSuite) AssertHTTPDispatchSpansGE(parents map[RequestSpanContext][]*testSpan, num int, operationID string) { spans := parents["dispatch_to_server"] if suite.Assert().GreaterOrEqual(num, len(spans)) { for i := 0; i < len(spans); i++ { suite.AssertHTTPDispatchSpan(spans[i], operationID) } } } func (suite *IntegrationTestSuite) AssertKVDispatchSpan(span *testSpan) { suite.Assert().Equal("dispatch_to_server", span.Name) suite.Assert().Equal(9, len(span.Tags)) suite.Assert().True(span.Finished) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().Equal("IP.TCP", span.Tags["net.transport"]) suite.Assert().NotEmpty(span.Tags["db.couchbase.operation_id"]) suite.Assert().NotEmpty(span.Tags["db.couchbase.local_id"]) suite.Assert().NotEmpty(span.Tags["net.host.name"]) suite.Assert().NotEmpty(span.Tags["net.host.port"]) suite.Assert().NotEmpty(span.Tags["net.peer.name"]) suite.Assert().NotEmpty(span.Tags["net.peer.port"]) suite.Assert().NotEmpty(span.Tags["db.couchbase.server_duration"]) } func (suite *IntegrationTestSuite) AssertHTTPDispatchSpan(span *testSpan, operationID string) { suite.Assert().Equal("dispatch_to_server", span.Name) suite.Assert().True(span.Finished) suite.Assert().Equal("couchbase", span.Tags["db.system"]) suite.Assert().Equal("IP.TCP", span.Tags["net.transport"]) suite.Assert().NotEmpty(span.Tags["net.peer.name"]) suite.Assert().NotEmpty(span.Tags["net.peer.port"]) spans := 5 if operationID == "" { suite.Assert().NotContains(span.Tags, "db.couchbase.operation_id") } else if operationID == "any" { spans++ suite.Assert().NotEmpty(span.Tags["db.couchbase.operation_id"]) } else { spans++ suite.Assert().Equal(operationID, span.Tags["db.couchbase.operation_id"]) } suite.Assert().Equal(spans, len(span.Tags)) } gocb-2.6.3/transaction_attemptcontext.go000066400000000000000000000212031441755043100204550ustar00rootroot00000000000000package gocb import ( "errors" "sync" "github.com/couchbase/gocbcore/v10" ) type transactionAttempt struct { State TransactionAttemptState PreExpiryAutoRollback bool Expired bool } type transactionQueryState struct { queryTarget string scope *Scope } // TransactionAttemptContext represents a single attempt to execute a transaction. type TransactionAttemptContext struct { txn *gocbcore.Transaction transcoder Transcoder cluster *Cluster hooks TransactionHooks // State applicable to when we move into query mode queryState *transactionQueryState // Pointer to satisfy go vet complaining about the hooks. queryStateLock *sync.Mutex queryConfig TransactionQueryOptions logger *transactionLogger attemptID string } func (c *TransactionAttemptContext) canCommit() bool { return c.txn.CanCommit() } func (c *TransactionAttemptContext) shouldRollback() bool { return c.txn.ShouldRollback() } func (c *TransactionAttemptContext) shouldRetry() bool { return c.txn.ShouldRetry() } func (c *TransactionAttemptContext) finalErrorToRaise() gocbcore.TransactionErrorReason { return c.txn.FinalErrorToRaise() } func (c *TransactionAttemptContext) attempt() transactionAttempt { a := c.txn.Attempt() return transactionAttempt{ State: TransactionAttemptState(a.State), PreExpiryAutoRollback: a.PreExpiryAutoRollback, Expired: c.txn.TimeRemaining() <= 0 || a.Expired, } } // Internal is used for internal dealings. // Internal: This should never be used and is not supported. func (c *TransactionAttemptContext) Internal() *InternalTransactionAttemptContext { return &InternalTransactionAttemptContext{ ac: c, } } // InternalTransactionAttemptContext is used for internal dealings. // Internal: This should never be used and is not supported. type InternalTransactionAttemptContext struct { ac *TransactionAttemptContext } func (iac *InternalTransactionAttemptContext) IsExpired() bool { return iac.ac.txn.HasExpired() } // Get will attempt to fetch a document, and fail the transaction if it does not exist. func (c *TransactionAttemptContext) Get(collection *Collection, id string) (*TransactionGetResult, error) { c.queryStateLock.Lock() if c.queryModeLocked() { res, err := c.getQueryMode(collection, id) if err != nil { c.logger.logInfof(c.attemptID, "Query mode get failed") c.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ ShouldNotCommit: !errors.Is(err, ErrDocumentNotFound), }) c.queryStateLock.Unlock() return nil, err } c.queryStateLock.Unlock() return res, nil } c.queryStateLock.Unlock() return c.get(collection, id) } func (c *TransactionAttemptContext) get(collection *Collection, id string) (resOut *TransactionGetResult, errOut error) { a, err := collection.Bucket().Internal().IORouter() if err != nil { return nil, err } waitCh := make(chan struct{}, 1) err = c.txn.Get(gocbcore.TransactionGetOptions{ Agent: a, ScopeName: collection.ScopeName(), CollectionName: collection.Name(), Key: []byte(id), }, func(res *gocbcore.TransactionGetResult, err error) { if err == nil { resOut = &TransactionGetResult{ collection: collection, docID: id, transcoder: NewJSONTranscoder(), flags: 2 << 24, coreRes: res, } } if errors.Is(err, ErrDocumentNotFound) { errOut = err waitCh <- struct{}{} return } errOut = createTransactionOperationFailedError(err) waitCh <- struct{}{} }) if err != nil { resOut = nil errOut = createTransactionOperationFailedError(err) return } <-waitCh return } // Replace will replace the contents of a document, failing if the document does not already exist. func (c *TransactionAttemptContext) Replace(doc *TransactionGetResult, value interface{}) (*TransactionGetResult, error) { // TODO: Use Transcoder here valueBytes, _, err := c.transcoder.Encode(value) if err != nil { return nil, err } c.queryStateLock.Lock() if c.queryModeLocked() { res, err := c.replaceQueryMode(doc, valueBytes) c.queryStateLock.Unlock() if err != nil { c.logger.logInfof(c.attemptID, "Query mode replace failed") return nil, err } return res, nil } c.queryStateLock.Unlock() return c.replace(doc, valueBytes) } func (c *TransactionAttemptContext) replace(doc *TransactionGetResult, valueBytes []byte) (resOut *TransactionGetResult, errOut error) { collection := doc.collection id := doc.docID waitCh := make(chan struct{}, 1) err := c.txn.Replace(gocbcore.TransactionReplaceOptions{ Document: doc.coreRes, Value: valueBytes, }, func(res *gocbcore.TransactionGetResult, err error) { if err == nil { resOut = &TransactionGetResult{ collection: collection, docID: id, transcoder: NewJSONTranscoder(), flags: 2 << 24, coreRes: res, } } errOut = createTransactionOperationFailedError(err) waitCh <- struct{}{} }) if err != nil { resOut = nil errOut = createTransactionOperationFailedError(err) return } <-waitCh return } // Insert will insert a new document, failing if the document already exists. func (c *TransactionAttemptContext) Insert(collection *Collection, id string, value interface{}) (*TransactionGetResult, error) { // TODO: Use Transcoder here valueBytes, _, err := c.transcoder.Encode(value) if err != nil { return nil, err } c.queryStateLock.Lock() if c.queryModeLocked() { res, err := c.insertQueryMode(collection, id, valueBytes) c.queryStateLock.Unlock() if err != nil { c.logger.logInfof(c.attemptID, "Query mode insert failed") return nil, err } return res, nil } c.queryStateLock.Unlock() return c.insert(collection, id, valueBytes) } func (c *TransactionAttemptContext) insert(collection *Collection, id string, valueBytes []byte) (resOut *TransactionGetResult, errOut error) { a, err := collection.Bucket().Internal().IORouter() if err != nil { return nil, err } waitCh := make(chan struct{}, 1) err = c.txn.Insert(gocbcore.TransactionInsertOptions{ Agent: a, ScopeName: collection.ScopeName(), CollectionName: collection.Name(), Key: []byte(id), Value: valueBytes, }, func(res *gocbcore.TransactionGetResult, err error) { if err == nil { resOut = &TransactionGetResult{ collection: collection, docID: id, transcoder: NewJSONTranscoder(), flags: 2 << 24, coreRes: res, } } // Handling for ExtInsertExisting if errors.Is(err, gocbcore.ErrDocumentExists) { errOut = err } else { errOut = createTransactionOperationFailedError(err) } waitCh <- struct{}{} }) if err != nil { resOut = nil errOut = createTransactionOperationFailedError(err) return } <-waitCh return } // Remove will delete a document. func (c *TransactionAttemptContext) Remove(doc *TransactionGetResult) error { c.queryStateLock.Lock() if c.queryModeLocked() { err := c.removeQueryMode(doc) c.queryStateLock.Unlock() if err != nil { c.logger.logInfof(c.attemptID, "Query mode remove failed") return err } return nil } c.queryStateLock.Unlock() return c.remove(doc) } func (c *TransactionAttemptContext) remove(doc *TransactionGetResult) (errOut error) { waitCh := make(chan struct{}, 1) err := c.txn.Remove(gocbcore.TransactionRemoveOptions{ Document: doc.coreRes, }, func(res *gocbcore.TransactionGetResult, err error) { errOut = createTransactionOperationFailedError(err) waitCh <- struct{}{} }) if err != nil { errOut = createTransactionOperationFailedError(err) return } <-waitCh return } func (c *TransactionAttemptContext) commit() (errOut error) { c.queryStateLock.Lock() if c.queryModeLocked() { err := c.commitQueryMode() c.queryStateLock.Unlock() if err != nil { c.logger.logInfof(c.attemptID, "Query mode commit failed") return err } return nil } c.queryStateLock.Unlock() waitCh := make(chan struct{}, 1) err := c.txn.Commit(func(err error) { errOut = createTransactionOperationFailedError(err) waitCh <- struct{}{} }) if err != nil { errOut = createTransactionOperationFailedError(err) return } <-waitCh return } func (c *TransactionAttemptContext) rollback() (errOut error) { c.queryStateLock.Lock() if c.queryModeLocked() { err := c.rollbackQueryMode() c.queryStateLock.Unlock() if err != nil { c.logger.logInfof(c.attemptID, "Query mode rollback failed") return err } return nil } c.queryStateLock.Unlock() waitCh := make(chan struct{}, 1) err := c.txn.Rollback(func(err error) { errOut = createTransactionOperationFailedError(err) waitCh <- struct{}{} }) if err != nil { errOut = createTransactionOperationFailedError(err) return } <-waitCh return } gocb-2.6.3/transaction_attemptcontext_query.go000066400000000000000000000471401441755043100217120ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "fmt" "time" "github.com/couchbase/gocbcore/v10" ) // Query executes the query statement on the server. func (c *TransactionAttemptContext) Query(statement string, options *TransactionQueryOptions) (*TransactionQueryResult, error) { c.logger.logInfof(c.attemptID, "Performing query: %s", redactUserDataString(statement)) var opts TransactionQueryOptions if options != nil { opts = *options } c.queryStateLock.Lock() res, err := c.queryWrapperWrapper(opts.Scope, statement, opts.toSDKOptions(), "query", false, true, nil) c.queryStateLock.Unlock() if err != nil { return nil, err } return res, nil } func (c *TransactionAttemptContext) queryModeLocked() bool { return c.queryState != nil } func (c *TransactionAttemptContext) getQueryMode(collection *Collection, id string) (*TransactionGetResult, error) { c.logger.logInfof(c.attemptID, "Performing query mode get: %s", newLoggableDocKey( collection.bucketName(), collection.ScopeName(), collection.Name(), id, )) txdata := map[string]interface{}{ "kv": true, } b, err := json.Marshal(txdata) if err != nil { // TODO: should we be wrapping this? It really shouldn't happen... return nil, err } handleErr := func(err error) error { var terr *TransactionOperationFailedError if errors.As(err, &terr) { return err } if errors.Is(err, ErrDocumentNotFound) { return err } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, }, c) } res, err := c.queryWrapperWrapper(c.queryState.scope, "EXECUTE __get", QueryOptions{ PositionalParameters: []interface{}{c.keyspace(collection), id}, Adhoc: true, }, "queryKvGet", false, true, b) if err != nil { return nil, handleErr(err) } type getQueryResult struct { Scas string `json:"scas"` Doc json.RawMessage `json:"doc"` TxnMeta json.RawMessage `json:"txnMeta,omitempty"` } var row getQueryResult err = res.One(&row) if err != nil { return nil, handleErr(err) } cas, err := fromScas(row.Scas) if err != nil { return nil, handleErr(err) } return &TransactionGetResult{ collection: collection, docID: id, transcoder: NewJSONTranscoder(), flags: 2 << 24, txnMeta: row.TxnMeta, coreRes: &gocbcore.TransactionGetResult{ Value: row.Doc, Cas: cas, }, }, nil } func (c *TransactionAttemptContext) replaceQueryMode(doc *TransactionGetResult, valueBytes json.RawMessage) (*TransactionGetResult, error) { c.logger.logInfof(c.attemptID, "Performing query mode replace: %s", newLoggableDocKey( doc.collection.bucketName(), doc.collection.ScopeName(), doc.collection.Name(), doc.docID, )) txdata := map[string]interface{}{ "kv": true, "scas": toScas(doc.coreRes.Cas), } if len(doc.txnMeta) > 0 { txdata["txnMeta"] = doc.txnMeta } b, err := json.Marshal(txdata) if err != nil { return nil, err } handleErr := func(err error) error { var terr *TransactionOperationFailedError if errors.As(err, &terr) { return err } if errors.Is(err, ErrDocumentNotFound) { return operationFailed(transactionQueryOperationFailedDef{ ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, ErrorClass: gocbcore.TransactionErrorClassFailDocNotFound, }, c) } else if errors.Is(err, ErrCasMismatch) { return operationFailed(transactionQueryOperationFailedDef{ ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, ErrorClass: gocbcore.TransactionErrorClassFailCasMismatch, }, c) } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, }, c) } params := []interface{}{c.keyspace(doc.collection), doc.docID, valueBytes, json.RawMessage("{}")} res, err := c.queryWrapperWrapper(c.queryState.scope, "EXECUTE __update", QueryOptions{ PositionalParameters: params, Adhoc: true, }, "queryKvReplace", false, true, b) if err != nil { return nil, handleErr(err) } type replaceQueryResult struct { Scas string `json:"scas"` Doc json.RawMessage `json:"doc"` } var row replaceQueryResult err = res.One(&row) if err != nil { return nil, handleErr(queryMaybeTranslateToTransactionsError(err, c)) } cas, err := fromScas(row.Scas) if err != nil { return nil, handleErr(err) } return &TransactionGetResult{ collection: doc.collection, docID: doc.docID, transcoder: NewJSONTranscoder(), flags: 2 << 24, coreRes: &gocbcore.TransactionGetResult{ Value: row.Doc, Cas: cas, }, }, nil } func (c *TransactionAttemptContext) insertQueryMode(collection *Collection, id string, valueBytes json.RawMessage) (*TransactionGetResult, error) { c.logger.logInfof(c.attemptID, "Performing query mode insert: %s", newLoggableDocKey( collection.bucketName(), collection.ScopeName(), collection.Name(), id, )) txdata := map[string]interface{}{ "kv": true, } b, err := json.Marshal(txdata) if err != nil { return nil, &TransactionOperationFailedError{ errorCause: err, } } handleErr := func(err error) error { var terr *TransactionOperationFailedError if errors.As(err, &terr) { return err } if errors.Is(err, ErrDocumentExists) { return err } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, }, c) } params := []interface{}{c.keyspace(collection), id, valueBytes, json.RawMessage("{}")} res, err := c.queryWrapperWrapper(c.queryState.scope, "EXECUTE __insert", QueryOptions{ PositionalParameters: params, Adhoc: true, }, "queryKvInsert", false, true, b) if err != nil { return nil, handleErr(err) } type insertQueryResult struct { Scas string `json:"scas"` } var row insertQueryResult err = res.One(&row) if err != nil { return nil, handleErr(queryMaybeTranslateToTransactionsError(err, c)) } cas, err := fromScas(row.Scas) if err != nil { return nil, handleErr(err) } return &TransactionGetResult{ collection: collection, docID: id, transcoder: NewJSONTranscoder(), flags: 2 << 24, coreRes: &gocbcore.TransactionGetResult{ Value: valueBytes, Cas: cas, }, }, nil } func (c *TransactionAttemptContext) removeQueryMode(doc *TransactionGetResult) error { c.logger.logInfof(c.attemptID, "Performing query mode remove: %s", newLoggableDocKey( doc.collection.bucketName(), doc.collection.ScopeName(), doc.collection.Name(), doc.docID, )) txdata := map[string]interface{}{ "kv": true, "scas": toScas(doc.coreRes.Cas), } if len(doc.txnMeta) > 0 { txdata["txnMeta"] = doc.txnMeta } b, err := json.Marshal(txdata) if err != nil { return err } handleErr := func(err error) error { var terr *TransactionOperationFailedError if errors.As(err, &terr) { return err } if errors.Is(err, ErrDocumentNotFound) { return operationFailed(transactionQueryOperationFailedDef{ ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, ErrorClass: gocbcore.TransactionErrorClassFailDocNotFound, }, c) } else if errors.Is(err, ErrCasMismatch) { return operationFailed(transactionQueryOperationFailedDef{ ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, ErrorClass: gocbcore.TransactionErrorClassFailCasMismatch, }, c) } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, }, c) } params := []interface{}{c.keyspace(doc.collection), doc.docID, json.RawMessage("{}")} _, err = c.queryWrapperWrapper(c.queryState.scope, "EXECUTE __delete", QueryOptions{ PositionalParameters: params, Adhoc: true, }, "queryKvRemove", false, true, b) if err != nil { return handleErr(err) } return nil } func (c *TransactionAttemptContext) commitQueryMode() error { c.logger.logInfof(c.attemptID, "Performing query mode commit") handleErr := func(err error) error { var terr *TransactionOperationFailedError if errors.As(err, &terr) { return err } if errors.Is(err, ErrAttemptExpired) { return operationFailed(transactionQueryOperationFailedDef{ ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionCommitAmbiguous, ShouldNotRollback: true, ShouldNotRetry: true, ErrorClass: gocbcore.TransactionErrorClassFailExpiry, }, c) } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ShouldNotRollback: true, ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, }, c) } _, err := c.queryWrapperWrapper(c.queryState.scope, "COMMIT", QueryOptions{ Adhoc: true, }, "queryCommit", false, true, nil) c.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ ShouldNotCommit: true, }) if err != nil { return handleErr(err) } c.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ ShouldNotRollback: true, ShouldNotRetry: true, State: gocbcore.TransactionAttemptStateCompleted, }) return nil } func (c *TransactionAttemptContext) rollbackQueryMode() error { c.logger.logInfof(c.attemptID, "Performing query mode rollback") handleErr := func(err error) error { var terr *TransactionOperationFailedError if errors.As(err, &terr) { return err } if errors.Is(err, ErrAttemptNotFoundOnQuery) { return nil } return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ShouldNotRollback: true, ErrorCause: err, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, }, c) } _, err := c.queryWrapperWrapper(c.queryState.scope, "ROLLBACK", QueryOptions{ Adhoc: true, }, "queryRollback", false, false, nil) c.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ ShouldNotRollback: true, ShouldNotCommit: true, }) if err != nil { return handleErr(err) } c.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ State: gocbcore.TransactionAttemptStateRolledBack, }) return nil } type jsonTransactionOperationFailed struct { Cause interface{} `json:"cause"` Retry bool `json:"retry"` Rollback bool `json:"rollback"` Raise string `json:"raise"` } type jsonQueryTransactionOperationFailedCause struct { Cause *jsonTransactionOperationFailed `json:"cause"` Code uint32 `json:"code"` Message string `json:"message"` } func durabilityLevelToQueryString(level gocbcore.TransactionDurabilityLevel) string { switch level { case gocbcore.TransactionDurabilityLevelUnknown: return "unset" case gocbcore.TransactionDurabilityLevelNone: return "none" case gocbcore.TransactionDurabilityLevelMajority: return "majority" case gocbcore.TransactionDurabilityLevelMajorityAndPersistToActive: return "majorityAndPersistActive" case gocbcore.TransactionDurabilityLevelPersistToMajority: return "persistToMajority" } return "" } // queryWrapperWrapper is used by any Query based calls on TransactionAttemptContext that require a non-streaming // result. It handles converting QueryResult To TransactionQueryResult, handling any errors that occur on the stream, // or because of a FATAL status in metadata. func (c *TransactionAttemptContext) queryWrapperWrapper(scope *Scope, statement string, options QueryOptions, hookPoint string, isBeginWork bool, existingErrorCheck bool, txData []byte) (*TransactionQueryResult, error) { result, err := c.queryWrapper(scope, statement, options, hookPoint, isBeginWork, existingErrorCheck, txData, false) if err != nil { return nil, err } var results []json.RawMessage for result.Next() { var r json.RawMessage err = result.Row(&r) if err != nil { return nil, queryMaybeTranslateToTransactionsError(err, c) } results = append(results, r) } if err := result.Err(); err != nil { return nil, queryMaybeTranslateToTransactionsError(err, c) } meta, err := result.MetaData() if err != nil { return nil, queryMaybeTranslateToTransactionsError(err, c) } if meta.Status == QueryStatusFatal { return nil, operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, }, c) } return newTransactionQueryResult(results, meta, result.endpoint), nil } // queryWrapper is used by every query based call on TransactionAttemptContext. It handles actually sending the // query as well as begin work and setting up query mode state. It returns a streaming QueryResult, handling only // errors that occur at query call time. func (c *TransactionAttemptContext) queryWrapper(scope *Scope, statement string, options QueryOptions, hookPoint string, isBeginWork bool, existingErrorCheck bool, txData []byte, txImplicit bool) (*QueryResult, error) { c.logger.logInfof(c.attemptID, "Query wrapped running %s, scope level = %t, begin work = %t, txImplicit = %t", redactUserDataString(statement), scope != nil, isBeginWork, txImplicit) var target string if !isBeginWork && !txImplicit { if !c.queryModeLocked() { // This is quite a big lock but we can't put the context into "query mode" until we know that begin work was // successful. We also can't allow any further ops to happen until we know if we're in "query mode" or not. // queryBeginWork implicitly performs an existingErrorCheck and the call into Serialize on the gocbcore side // will return an error if there have been any previously failed operations. if err := c.queryBeginWork(scope); err != nil { return nil, err } } // If we've got here then transactionQueryState cannot be nil. target = c.queryState.queryTarget c.logger.logInfof(c.attemptID, "Using query target %s", redactSystemDataString(target)) if !c.txn.CanCommit() && !c.txn.ShouldRollback() { c.logger.logInfof(c.attemptID, "Transaction marked cannot commit and should not rollback, failing") return nil, operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ErrorCause: ErrOther, ErrorClass: gocbcore.TransactionErrorClassFailOther, ShouldNotRollback: true, }, c) } } if existingErrorCheck { if !c.txn.CanCommit() { c.logger.logInfof(c.attemptID, "Transaction marked cannot commit during existing error check, failing") return nil, operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ErrorCause: ErrPreviousOperationFailed, ErrorClass: gocbcore.TransactionErrorClassFailOther, }, c) } } expired, err := c.hooks.HasExpiredClientSideHook(*c, hookPoint, statement) if err != nil { // This isn't meant to happen... return nil, &TransactionOperationFailedError{ errorCause: err, } } cfg := c.txn.Config() if cfg.ExpirationTime < 10*time.Millisecond || expired { c.logger.logInfof(c.attemptID, "Transaction expired, failing") return nil, operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ShouldNotRollback: true, Reason: gocbcore.TransactionErrorReasonTransactionExpired, ErrorCause: ErrAttemptExpired, ErrorClass: gocbcore.TransactionErrorClassFailExpiry, }, c) } options.Metrics = true options.Internal.Endpoint = target if options.Raw == nil { options.Raw = make(map[string]interface{}) } if !isBeginWork && !txImplicit { options.Raw["txid"] = c.txn.Attempt().ID } if len(txData) > 0 { options.Raw["txdata"] = json.RawMessage(txData) } if txImplicit { options.Raw["tximplicit"] = true if options.ScanConsistency == 0 { options.ScanConsistency = QueryScanConsistencyRequestPlus } options.Raw["durability_level"] = durabilityLevelToQueryString(cfg.DurabilityLevel) options.Raw["txtimeout"] = fmt.Sprintf("%dms", cfg.ExpirationTime.Milliseconds()) if cfg.CustomATRLocation.Agent != nil { // Agent being non nil signifies that this was set. options.Raw["atrcollection"] = fmt.Sprintf( "%s.%s.%s", cfg.CustomATRLocation.Agent.BucketName(), cfg.CustomATRLocation.ScopeName, cfg.CustomATRLocation.CollectionName, ) } // Need to make sure we don't end up straight back here... options.AsTransaction = nil } options.Timeout = cfg.ExpirationTime + cfg.KeyValueTimeout + (1 * time.Second) err = c.hooks.BeforeQuery(*c, statement) if err != nil { return nil, queryMaybeTranslateToTransactionsError(err, c) } var result *QueryResult var queryErr error if scope == nil { result, queryErr = c.cluster.Query(statement, &options) } else { result, queryErr = scope.Query(statement, &options) } if queryErr != nil { return nil, queryMaybeTranslateToTransactionsError(queryErr, c) } err = c.hooks.AfterQuery(*c, statement) if err != nil { return nil, queryMaybeTranslateToTransactionsError(err, c) } return result, nil } func (c *TransactionAttemptContext) queryBeginWork(scope *Scope) (errOut error) { c.logger.logInfof(c.attemptID, "Performing query begin work") waitCh := make(chan struct{}, 1) err := c.txn.SerializeAttempt(func(txdata []byte, err error) { if err != nil { c.logger.logInfof(c.attemptID, "SerializeAttempt failed, not moving into query mode") var coreErr *gocbcore.TransactionOperationFailedError if errors.As(err, &coreErr) { // Note that we purposely do not use operationFailed here, we haven't moved into query mode yet. // State will continue to be controlled from the gocbcore side. errOut = &TransactionOperationFailedError{ shouldRetry: coreErr.Retry(), shouldNotRollback: !coreErr.Rollback(), errorCause: coreErr.InternalUnwrap(), shouldRaise: coreErr.ToRaise(), errorClass: coreErr.ErrorClass(), } } else { errOut = err } waitCh <- struct{}{} return } // Store any scope for later operations. c.queryState = &transactionQueryState{ scope: scope, } cfg := c.txn.Config() raw := make(map[string]interface{}) raw["durability_level"] = durabilityLevelToQueryString(cfg.DurabilityLevel) raw["txtimeout"] = fmt.Sprintf("%dms", cfg.ExpirationTime.Milliseconds()) if cfg.CustomATRLocation.Agent != nil { // Agent being non nil signifies that this was set. raw["atrcollection"] = fmt.Sprintf( "%s.%s.%s", cfg.CustomATRLocation.Agent.BucketName(), cfg.CustomATRLocation.ScopeName, cfg.CustomATRLocation.CollectionName, ) } res, err := c.queryWrapperWrapper(scope, "BEGIN WORK", QueryOptions{ ScanConsistency: c.queryConfig.ScanConsistency, Raw: raw, Adhoc: true, }, "queryBeginWork", true, false, txdata) if err != nil { errOut = err waitCh <- struct{}{} return } c.logger.logInfof(c.attemptID, "Begin work setting query target to %s", res.endpoint) c.queryState.queryTarget = res.endpoint waitCh <- struct{}{} }) if err != nil { errOut = err return } <-waitCh return } func (c *TransactionAttemptContext) keyspace(collection *Collection) string { return fmt.Sprintf("default:`%s`.`%s`.`%s`", collection.Bucket().Name(), collection.ScopeName(), collection.Name()) } gocb-2.6.3/transaction_getresult.go000066400000000000000000000014471441755043100174200ustar00rootroot00000000000000package gocb import ( "encoding/json" "strconv" "github.com/couchbase/gocbcore/v10" ) // TransactionGetResult represents the result of a Get operation which was performed. type TransactionGetResult struct { collection *Collection docID string transcoder Transcoder flags uint32 txnMeta json.RawMessage coreRes *gocbcore.TransactionGetResult } // Content provides access to the documents contents. func (d *TransactionGetResult) Content(valuePtr interface{}) error { return d.transcoder.Decode(d.coreRes.Value, d.flags, valuePtr) } func fromScas(scas string) (gocbcore.Cas, error) { i, err := strconv.ParseUint(scas, 10, 64) if err != nil { return 0, err } return gocbcore.Cas(i), nil } func toScas(cas gocbcore.Cas) string { return strconv.FormatUint(uint64(cas), 10) } gocb-2.6.3/transaction_hooks.go000066400000000000000000000434301441755043100165230ustar00rootroot00000000000000package gocb import ( "github.com/couchbase/gocbcore/v10" ) // TransactionHooks provides a number of internal hooks used for testing. // Internal: This should never be used and is not supported. type TransactionHooks interface { BeforeATRCommit(ctx TransactionAttemptContext) error AfterATRCommit(ctx TransactionAttemptContext) error BeforeDocCommitted(ctx TransactionAttemptContext, docID string) error BeforeRemovingDocDuringStagedInsert(ctx TransactionAttemptContext, docID string) error BeforeRollbackDeleteInserted(ctx TransactionAttemptContext, docID string) error AfterDocCommittedBeforeSavingCAS(ctx TransactionAttemptContext, docID string) error AfterDocCommitted(ctx TransactionAttemptContext, docID string) error BeforeStagedInsert(ctx TransactionAttemptContext, docID string) error BeforeStagedRemove(ctx TransactionAttemptContext, docID string) error BeforeStagedReplace(ctx TransactionAttemptContext, docID string) error BeforeDocRemoved(ctx TransactionAttemptContext, docID string) error BeforeDocRolledBack(ctx TransactionAttemptContext, docID string) error AfterDocRemovedPreRetry(ctx TransactionAttemptContext, docID string) error AfterDocRemovedPostRetry(ctx TransactionAttemptContext, docID string) error AfterGetComplete(ctx TransactionAttemptContext, docID string) error AfterStagedReplaceComplete(ctx TransactionAttemptContext, docID string) error AfterStagedRemoveComplete(ctx TransactionAttemptContext, docID string) error AfterStagedInsertComplete(ctx TransactionAttemptContext, docID string) error AfterRollbackReplaceOrRemove(ctx TransactionAttemptContext, docID string) error AfterRollbackDeleteInserted(ctx TransactionAttemptContext, docID string) error BeforeCheckATREntryForBlockingDoc(ctx TransactionAttemptContext, docID string) error BeforeDocGet(ctx TransactionAttemptContext, docID string) error BeforeGetDocInExistsDuringStagedInsert(ctx TransactionAttemptContext, docID string) error BeforeRemoveStagedInsert(ctx TransactionAttemptContext, docID string) error AfterRemoveStagedInsert(ctx TransactionAttemptContext, docID string) error AfterDocsCommitted(ctx TransactionAttemptContext) error AfterDocsRemoved(ctx TransactionAttemptContext) error AfterATRPending(ctx TransactionAttemptContext) error BeforeATRPending(ctx TransactionAttemptContext) error BeforeATRComplete(ctx TransactionAttemptContext) error BeforeATRRolledBack(ctx TransactionAttemptContext) error AfterATRComplete(ctx TransactionAttemptContext) error BeforeATRAborted(ctx TransactionAttemptContext) error AfterATRAborted(ctx TransactionAttemptContext) error AfterATRRolledBack(ctx TransactionAttemptContext) error BeforeATRCommitAmbiguityResolution(ctx TransactionAttemptContext) error RandomATRIDForVbucket(ctx TransactionAttemptContext) (string, error) HasExpiredClientSideHook(ctx TransactionAttemptContext, stage string, vbID string) (bool, error) BeforeQuery(ctx TransactionAttemptContext, statement string) error AfterQuery(ctx TransactionAttemptContext, statement string) error } // TransactionCleanupHooks provides a number of internal hooks used for testing. // Internal: This should never be used and is not supported. type TransactionCleanupHooks interface { BeforeATRGet(id string) error BeforeDocGet(id string) error BeforeRemoveLinks(id string) error BeforeCommitDoc(id string) error BeforeRemoveDocStagedForRemoval(id string) error BeforeRemoveDoc(id string) error BeforeATRRemove(id string) error } // TransactionClientRecordHooks provides a number of internal hooks used for testing. // Internal: This should never be used and is not supported. type TransactionClientRecordHooks interface { BeforeCreateRecord() error BeforeRemoveClient() error BeforeUpdateCAS() error BeforeGetRecord() error BeforeUpdateRecord() error } type transactionHooksWrapper interface { SetAttemptContext(ctx TransactionAttemptContext) gocbcore.TransactionHooks Hooks() TransactionHooks } type transactionCleanupHooksWrapper interface { gocbcore.TransactionCleanUpHooks } type coreTxnsHooksWrapper struct { ctx TransactionAttemptContext hooks TransactionHooks } type clientRecordHooksWrapper interface { gocbcore.TransactionClientRecordHooks } func (cthw *coreTxnsHooksWrapper) SetAttemptContext(ctx TransactionAttemptContext) { cthw.ctx = ctx } func (cthw *coreTxnsHooksWrapper) Hooks() TransactionHooks { return cthw.hooks } func (cthw *coreTxnsHooksWrapper) BeforeATRCommit(cb func(err error)) { go func() { cb(cthw.hooks.BeforeATRCommit(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) AfterATRCommit(cb func(err error)) { go func() { cb(cthw.hooks.AfterATRCommit(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) BeforeDocCommitted(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeDocCommitted(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeRemovingDocDuringStagedInsert(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeRemovingDocDuringStagedInsert(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeRollbackDeleteInserted(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeRollbackDeleteInserted(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterDocCommittedBeforeSavingCAS(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterDocCommittedBeforeSavingCAS(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterDocCommitted(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterDocCommitted(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeStagedInsert(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeStagedInsert(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeStagedRemove(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeStagedRemove(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeStagedReplace(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeStagedReplace(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeDocRemoved(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeDocRemoved(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeDocRolledBack(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeDocRolledBack(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterDocRemovedPreRetry(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterDocRemovedPreRetry(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterDocRemovedPostRetry(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterDocRemovedPostRetry(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterGetComplete(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterGetComplete(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterStagedReplaceComplete(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterStagedReplaceComplete(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterStagedRemoveComplete(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterStagedRemoveComplete(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterStagedInsertComplete(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterStagedInsertComplete(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterRollbackReplaceOrRemove(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterRollbackReplaceOrRemove(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterRollbackDeleteInserted(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterRollbackDeleteInserted(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeCheckATREntryForBlockingDoc(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeCheckATREntryForBlockingDoc(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeDocGet(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeDocGet(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeGetDocInExistsDuringStagedInsert(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeGetDocInExistsDuringStagedInsert(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) BeforeRemoveStagedInsert(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.BeforeRemoveStagedInsert(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterRemoveStagedInsert(docID []byte, cb func(err error)) { go func() { cb(cthw.hooks.AfterRemoveStagedInsert(cthw.ctx, string(docID))) }() } func (cthw *coreTxnsHooksWrapper) AfterDocsCommitted(cb func(err error)) { go func() { cb(cthw.hooks.AfterDocsCommitted(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) AfterDocsRemoved(cb func(err error)) { go func() { cb(cthw.hooks.AfterDocsRemoved(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) AfterATRPending(cb func(err error)) { go func() { cb(cthw.hooks.AfterATRPending(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) BeforeATRPending(cb func(err error)) { go func() { cb(cthw.hooks.BeforeATRPending(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) BeforeATRComplete(cb func(err error)) { go func() { cb(cthw.hooks.BeforeATRComplete(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) BeforeATRRolledBack(cb func(err error)) { go func() { cb(cthw.hooks.BeforeATRRolledBack(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) AfterATRComplete(cb func(err error)) { go func() { cb(cthw.hooks.AfterATRComplete(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) BeforeATRAborted(cb func(err error)) { go func() { cb(cthw.hooks.BeforeATRAborted(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) AfterATRAborted(cb func(err error)) { go func() { cb(cthw.hooks.AfterATRAborted(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) AfterATRRolledBack(cb func(err error)) { go func() { cb(cthw.hooks.AfterATRRolledBack(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) BeforeATRCommitAmbiguityResolution(cb func(err error)) { go func() { cb(cthw.hooks.BeforeATRCommitAmbiguityResolution(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) RandomATRIDForVbucket(cb func(string, error)) { go func() { cb(cthw.hooks.RandomATRIDForVbucket(cthw.ctx)) }() } func (cthw *coreTxnsHooksWrapper) HasExpiredClientSideHook(stage string, vbID []byte, cb func(bool, error)) { go func() { cb(cthw.hooks.HasExpiredClientSideHook(cthw.ctx, stage, string(vbID))) }() } type coreTxnsCleanupHooksWrapper struct { CleanupHooks TransactionCleanupHooks } func (cthw *coreTxnsCleanupHooksWrapper) BeforeATRGet(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeATRGet(string(id))) }() } func (cthw *coreTxnsCleanupHooksWrapper) BeforeDocGet(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeDocGet(string(id))) }() } func (cthw *coreTxnsCleanupHooksWrapper) BeforeRemoveLinks(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeRemoveLinks(string(id))) }() } func (cthw *coreTxnsCleanupHooksWrapper) BeforeCommitDoc(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeCommitDoc(string(id))) }() } func (cthw *coreTxnsCleanupHooksWrapper) BeforeRemoveDocStagedForRemoval(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeRemoveDocStagedForRemoval(string(id))) }() } func (cthw *coreTxnsCleanupHooksWrapper) BeforeRemoveDoc(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeRemoveDoc(string(id))) }() } func (cthw *coreTxnsCleanupHooksWrapper) BeforeATRRemove(id []byte, cb func(error)) { go func() { cb(cthw.CleanupHooks.BeforeATRRemove(string(id))) }() } type coreTxnsClientRecordHooksWrapper struct { coreTxnsCleanupHooksWrapper ClientRecordHooks TransactionClientRecordHooks } func (hw *coreTxnsClientRecordHooksWrapper) BeforeCreateRecord(cb func(error)) { go func() { cb(hw.ClientRecordHooks.BeforeCreateRecord()) }() } func (hw *coreTxnsClientRecordHooksWrapper) BeforeRemoveClient(cb func(error)) { go func() { cb(hw.ClientRecordHooks.BeforeRemoveClient()) }() } func (hw *coreTxnsClientRecordHooksWrapper) BeforeUpdateCAS(cb func(error)) { go func() { cb(hw.ClientRecordHooks.BeforeUpdateCAS()) }() } func (hw *coreTxnsClientRecordHooksWrapper) BeforeGetRecord(cb func(error)) { go func() { cb(hw.ClientRecordHooks.BeforeGetRecord()) }() } func (hw *coreTxnsClientRecordHooksWrapper) BeforeUpdateRecord(cb func(error)) { go func() { cb(hw.ClientRecordHooks.BeforeUpdateRecord()) }() } type noopHooksWrapper struct { gocbcore.TransactionDefaultHooks hooks transactionsDefaultHooks } func (nhw *noopHooksWrapper) SetAttemptContext(ctx TransactionAttemptContext) { } func (nhw *noopHooksWrapper) Hooks() TransactionHooks { return nhw.hooks } type noopCleanupHooksWrapper struct { gocbcore.TransactionDefaultCleanupHooks } type noopClientRecordHooksWrapper struct { gocbcore.TransactionDefaultCleanupHooks gocbcore.TransactionDefaultClientRecordHooks } type transactionsDefaultHooks struct { } func (d transactionsDefaultHooks) BeforeATRCommit(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) AfterATRCommit(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) BeforeDocCommitted(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeRemovingDocDuringStagedInsert(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeRollbackDeleteInserted(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterDocCommittedBeforeSavingCAS(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterDocCommitted(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeStagedInsert(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeStagedRemove(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeStagedReplace(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeDocRemoved(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeDocRolledBack(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterDocRemovedPreRetry(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterDocRemovedPostRetry(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterGetComplete(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterStagedReplaceComplete(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterStagedRemoveComplete(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterStagedInsertComplete(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterRollbackReplaceOrRemove(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterRollbackDeleteInserted(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeCheckATREntryForBlockingDoc(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeDocGet(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeGetDocInExistsDuringStagedInsert(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) BeforeRemoveStagedInsert(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterRemoveStagedInsert(ctx TransactionAttemptContext, docID string) error { return nil } func (d transactionsDefaultHooks) AfterDocsCommitted(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) AfterDocsRemoved(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) AfterATRPending(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) BeforeATRPending(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) BeforeATRComplete(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) BeforeATRRolledBack(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) AfterATRComplete(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) BeforeATRAborted(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) AfterATRAborted(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) AfterATRRolledBack(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) BeforeATRCommitAmbiguityResolution(ctx TransactionAttemptContext) error { return nil } func (d transactionsDefaultHooks) RandomATRIDForVbucket(ctx TransactionAttemptContext) (string, error) { return "", nil } func (d transactionsDefaultHooks) HasExpiredClientSideHook(ctx TransactionAttemptContext, stage string, vbID string) (bool, error) { return false, nil } func (d transactionsDefaultHooks) BeforeQuery(ctx TransactionAttemptContext, statement string) error { return nil } func (d transactionsDefaultHooks) AfterQuery(ctx TransactionAttemptContext, statement string) error { return nil } gocb-2.6.3/transaction_logger.go000066400000000000000000000066701441755043100166640ustar00rootroot00000000000000// nolint: unused package gocb import ( "fmt" "log" "sync" "time" "github.com/couchbase/gocbcore/v10" ) type loggableDocKey struct { bucket string scope string collection string id string } func newLoggableDocKey(bucket, scope, collection string, id string) loggableDocKey { return loggableDocKey{ bucket: bucket, scope: scope, collection: collection, id: id, } } func (rdi loggableDocKey) String() string { scope := rdi.scope if scope == "" { scope = "_default" } collection := rdi.collection if collection == "" { collection = "_default" } return redactUserDataString(rdi.bucket + "." + scope + "." + collection + "." + rdi.id) } // TransactionLogger is the logger used for logging in transactions. type TransactionLogger interface { Logs() []TransactionLogItem } // TransactionLogItem represents an entry in the transaction in memory logging. type TransactionLogItem struct { Level LogLevel args []interface{} txnID string attemptID string timestamp time.Time fmt string } func (item TransactionLogItem) String() string { return fmt.Sprintf("%s %s/%s %s", item.timestamp.AppendFormat([]byte{}, "15:04:05.000"), item.txnID, item.attemptID, fmt.Sprintf(item.fmt, item.args...)) } // transactionLogger log to memory, also logging WARN and ERROR logs to the SDK logger. type transactionLogger struct { lock sync.Mutex items []TransactionLogItem logDirectlyBelowLevel gocbcore.LogLevel txnID string } func newTransactionLogger() *transactionLogger { return &transactionLogger{ logDirectlyBelowLevel: gocbcore.LogInfo, items: make([]TransactionLogItem, 0, 256), } } func (tl *transactionLogger) setTxnID(txnID string) { tl.txnID = txnID[:5] } func (tl *transactionLogger) Logs() []TransactionLogItem { tl.lock.Lock() logs := make([]TransactionLogItem, len(tl.items)) copy(logs, tl.items) tl.lock.Unlock() return logs } func (tl *transactionLogger) Log(level gocbcore.LogLevel, offset int, txnID, attemptID, fmt string, args ...interface{}) error { item := TransactionLogItem{ Level: LogLevel(level), args: args, txnID: txnID, attemptID: attemptID, timestamp: time.Now(), fmt: fmt, } tl.lock.Lock() tl.items = append(tl.items, item) tl.lock.Unlock() if level <= gocbcore.LogWarn { logExf(LogLevel(level), offset, txnID+"/"+attemptID+" "+fmt, args...) } return nil } func (tl *transactionLogger) logExf(attemptID string, level gocbcore.LogLevel, fmt string, args ...interface{}) { if attemptID != "" { attemptID = attemptID[:5] } err := tl.Log(level, 1, tl.txnID, attemptID, fmt, args...) if err != nil { log.Printf("Transaction logger error occurred (%s)\n", err) } } func (tl *transactionLogger) logDebugf(attemptID, format string, v ...interface{}) { tl.logExf(attemptID, gocbcore.LogDebug, format, v...) } func (tl *transactionLogger) logSchedf(attemptID, format string, v ...interface{}) { tl.logExf(attemptID, gocbcore.LogSched, format, v...) } func (tl *transactionLogger) logWarnf(attemptID, format string, v ...interface{}) { tl.logExf(attemptID, gocbcore.LogWarn, format, v...) } func (tl *transactionLogger) logErrorf(attemptID, format string, v ...interface{}) { tl.logExf(attemptID, gocbcore.LogError, format, v...) } func (tl *transactionLogger) logInfof(attemptID, format string, v ...interface{}) { tl.logExf(attemptID, gocbcore.LogInfo, format, v...) } gocb-2.6.3/transaction_queryresult.go000066400000000000000000000031211441755043100177750ustar00rootroot00000000000000package gocb import ( "encoding/json" ) // TransactionQueryResult allows access to the results of a query. type TransactionQueryResult struct { results []json.RawMessage idx int rowBytes json.RawMessage metadata *QueryMetaData endpoint string } func newTransactionQueryResult(results []json.RawMessage, meta *QueryMetaData, endpoint string) *TransactionQueryResult { return &TransactionQueryResult{ results: results, metadata: meta, endpoint: endpoint, } } // Next assigns the next result from the results into the value pointer, returning whether the read was successful. func (r *TransactionQueryResult) Next() bool { if r.idx >= len(r.results) { return false } r.rowBytes = r.results[r.idx] r.idx++ return true } // Row returns the contents of the current row func (r *TransactionQueryResult) Row(valuePtr interface{}) error { if r.rowBytes == nil { return ErrNoResult } if bytesPtr, ok := valuePtr.(*json.RawMessage); ok { *bytesPtr = r.rowBytes return nil } return json.Unmarshal(r.rowBytes, valuePtr) } // One assigns the first value from the results into the value pointer. func (r *TransactionQueryResult) One(valuePtr interface{}) error { // Prime the row if !r.Next() { return ErrNoResult } err := r.Row(valuePtr) if err != nil { return err } return nil } // MetaData returns any meta-data that was available from this query. Note that // the meta-data will only be available once the object has been closed (either // implicitly or explicitly). func (r *TransactionQueryResult) MetaData() (*QueryMetaData, error) { return r.metadata, nil } gocb-2.6.3/transaction_result.go000066400000000000000000000047771441755043100167310ustar00rootroot00000000000000package gocb import ( "github.com/couchbase/gocbcore/v10" ) // TransactionAttemptState represents the current state of a transaction attempt. // Internal: This should never be used and is not supported. type TransactionAttemptState int const ( // TransactionAttemptStateNothingWritten indicates that nothing has been written in this attempt. // Internal: This should never be used and is not supported. TransactionAttemptStateNothingWritten = TransactionAttemptState(gocbcore.TransactionAttemptStateNothingWritten) // TransactionAttemptStatePending indicates that this attempt is in pending state. // Internal: This should never be used and is not supported. TransactionAttemptStatePending = TransactionAttemptState(gocbcore.TransactionAttemptStatePending) // TransactionAttemptStateCommitting indicates that this attempt is in committing state. // Internal: This should never be used and is not supported. TransactionAttemptStateCommitting = TransactionAttemptState(gocbcore.TransactionAttemptStateCommitting) // TransactionAttemptStateCommitted indicates that this attempt is in committed state. // Internal: This should never be used and is not supported. TransactionAttemptStateCommitted = TransactionAttemptState(gocbcore.TransactionAttemptStateCommitted) // TransactionAttemptStateCompleted indicates that this attempt is in completed state. // Internal: This should never be used and is not supported. TransactionAttemptStateCompleted = TransactionAttemptState(gocbcore.TransactionAttemptStateCompleted) // TransactionAttemptStateAborted indicates that this attempt is in aborted state. // Internal: This should never be used and is not supported. TransactionAttemptStateAborted = TransactionAttemptState(gocbcore.TransactionAttemptStateAborted) // TransactionAttemptStateRolledBack indicates that this attempt is in rolled back state. // Internal: This should never be used and is not supported. TransactionAttemptStateRolledBack = TransactionAttemptState(gocbcore.TransactionAttemptStateRolledBack) ) // TransactionResult represents the result of a transaction which was executed. type TransactionResult struct { // TransactionID represents the UUID assigned to this transaction TransactionID string // UnstagingComplete indicates whether the transaction was succesfully // unstaged, or if a later cleanup job will be responsible. UnstagingComplete bool // Logs returns the set of logs that were created during this transaction. // UNCOMMITTED: This API may change in the future. Logs []TransactionLogItem } gocb-2.6.3/transactions.go000066400000000000000000000361611441755043100155060ustar00rootroot00000000000000package gocb import ( "errors" "math" "sync" "time" "github.com/couchbase/gocbcore/v10" ) // AttemptFunc represents the lambda used by the Transactions Run function. type AttemptFunc func(*TransactionAttemptContext) error // Transactions can be used to perform transactions. type Transactions struct { config TransactionsConfig cluster *Cluster transcoder Transcoder txns *gocbcore.TransactionsManager hooksWrapper transactionHooksWrapper cleanupHooksWrapper transactionCleanupHooksWrapper cleanupCollections []gocbcore.TransactionLostATRLocation } // initTransactions will initialize the transactions library and return a Transactions // object which can be used to perform transactions. func (c *Cluster) initTransactions(config TransactionsConfig) (*Transactions, error) { // Note that gocbcore will handle a lot of default values for us. if config.QueryConfig.ScanConsistency == 0 { config.QueryConfig.ScanConsistency = QueryScanConsistencyRequestPlus } if config.DurabilityLevel == DurabilityLevelUnknown { config.DurabilityLevel = DurabilityLevelMajority } var hooksWrapper transactionHooksWrapper if config.Internal.Hooks == nil { hooksWrapper = &noopHooksWrapper{ TransactionDefaultHooks: gocbcore.TransactionDefaultHooks{}, hooks: transactionsDefaultHooks{}, } } else { hooksWrapper = &coreTxnsHooksWrapper{ hooks: config.Internal.Hooks, } } var cleanupHooksWrapper transactionCleanupHooksWrapper if config.Internal.CleanupHooks == nil { cleanupHooksWrapper = &noopCleanupHooksWrapper{ TransactionDefaultCleanupHooks: gocbcore.TransactionDefaultCleanupHooks{}, } } else { cleanupHooksWrapper = &coreTxnsCleanupHooksWrapper{ CleanupHooks: config.Internal.CleanupHooks, } } var clientRecordHooksWrapper clientRecordHooksWrapper if config.Internal.ClientRecordHooks == nil { clientRecordHooksWrapper = &noopClientRecordHooksWrapper{ TransactionDefaultCleanupHooks: gocbcore.TransactionDefaultCleanupHooks{}, TransactionDefaultClientRecordHooks: gocbcore.TransactionDefaultClientRecordHooks{}, } } else { clientRecordHooksWrapper = &coreTxnsClientRecordHooksWrapper{ coreTxnsCleanupHooksWrapper: coreTxnsCleanupHooksWrapper{ CleanupHooks: config.Internal.CleanupHooks, }, ClientRecordHooks: config.Internal.ClientRecordHooks, } } atrLocation := gocbcore.TransactionATRLocation{} if config.MetadataCollection != nil { customATRAgent, err := c.Bucket(config.MetadataCollection.BucketName).Internal().IORouter() if err != nil { return nil, err } atrLocation.Agent = customATRAgent atrLocation.CollectionName = config.MetadataCollection.CollectionName atrLocation.ScopeName = config.MetadataCollection.ScopeName // We add the custom metadata collection to the cleanup collections so that lost cleanup starts watching it // immediately. Note that we don't do the same for the custom metadata on TransactionOptions, this is because // we know that that collection will be used in a transaction. var alreadyInCleanup bool for _, keySpace := range config.CleanupConfig.CleanupCollections { if keySpace == *config.MetadataCollection { alreadyInCleanup = true break } } if !alreadyInCleanup { config.CleanupConfig.CleanupCollections = append(config.CleanupConfig.CleanupCollections, *config.MetadataCollection) } } var cleanupLocs []gocbcore.TransactionLostATRLocation for _, keyspace := range config.CleanupConfig.CleanupCollections { cleanupLocs = append(cleanupLocs, gocbcore.TransactionLostATRLocation{ BucketName: keyspace.BucketName, ScopeName: keyspace.ScopeName, CollectionName: keyspace.CollectionName, }) } t := &Transactions{ cluster: c, config: config, transcoder: NewJSONTranscoder(), hooksWrapper: hooksWrapper, cleanupHooksWrapper: cleanupHooksWrapper, cleanupCollections: cleanupLocs, } corecfg := &gocbcore.TransactionsConfig{} corecfg.DurabilityLevel = gocbcore.TransactionDurabilityLevel(config.DurabilityLevel) corecfg.BucketAgentProvider = t.agentProvider corecfg.LostCleanupATRLocationProvider = t.atrLocationsProvider corecfg.CleanupClientAttempts = !config.CleanupConfig.DisableClientAttemptCleanup corecfg.CleanupQueueSize = config.CleanupConfig.CleanupQueueSize corecfg.ExpirationTime = config.Timeout corecfg.CleanupWindow = config.CleanupConfig.CleanupWindow corecfg.CleanupLostAttempts = !config.CleanupConfig.DisableLostAttemptCleanup corecfg.CustomATRLocation = atrLocation corecfg.Internal.Hooks = hooksWrapper corecfg.Internal.CleanUpHooks = cleanupHooksWrapper corecfg.Internal.ClientRecordHooks = clientRecordHooksWrapper corecfg.Internal.NumATRs = config.Internal.NumATRs corecfg.KeyValueTimeout = c.timeoutsConfig.KVTimeout txns, err := gocbcore.InitTransactions(corecfg) if err != nil { return nil, err } t.txns = txns return t, nil } // Run runs a lambda to perform a number of operations as part of a // singular transaction. func (t *Transactions) Run(logicFn AttemptFunc, perConfig *TransactionOptions) (*TransactionResult, error) { return t.run(logicFn, perConfig, false) } func (t *Transactions) run(logicFn AttemptFunc, perConfig *TransactionOptions, singleQueryMode bool) (*TransactionResult, error) { if perConfig == nil { perConfig = &TransactionOptions{ DurabilityLevel: t.config.DurabilityLevel, Timeout: t.config.Timeout, } } scanConsistency := t.config.QueryConfig.ScanConsistency // Gocbcore looks at whether the location agent is nil to verify whether CustomATRLocation has been set. atrLocation := gocbcore.TransactionATRLocation{} if perConfig.MetadataCollection != nil { customATRAgent, err := perConfig.MetadataCollection.bucket.Internal().IORouter() if err != nil { return nil, err } atrLocation.Agent = customATRAgent atrLocation.CollectionName = perConfig.MetadataCollection.Name() atrLocation.ScopeName = perConfig.MetadataCollection.ScopeName() } logger := newTransactionLogger() // TODO: fill in the rest of this config config := &gocbcore.TransactionOptions{ DurabilityLevel: gocbcore.TransactionDurabilityLevel(perConfig.DurabilityLevel), ExpirationTime: perConfig.Timeout, CustomATRLocation: atrLocation, TransactionLogger: logger, } hooksWrapper := t.hooksWrapper if perConfig.Internal.Hooks != nil { hooksWrapper = &coreTxnsHooksWrapper{ hooks: perConfig.Internal.Hooks, } config.Internal.Hooks = hooksWrapper } txn, err := t.txns.BeginTransaction(config) if err != nil { return nil, err } logger.setTxnID(txn.ID()) retries := 0 backoffCalc := func() time.Duration { var max float64 = 100000000 // 100 Milliseconds var min float64 = 1000000 // 1 Millisecond retries++ backoff := min * (math.Pow(2, float64(retries))) if backoff > max { backoff = max } if backoff < min { backoff = min } return time.Duration(backoff) } for { err = txn.NewAttempt() if err != nil { return nil, err } attemptID := txn.Attempt().ID logDebugf("New transaction attempt starting for %s, %s", txn.ID(), attemptID) logger.logInfof(attemptID, "New transaction attempt starting") attempt := TransactionAttemptContext{ txn: txn, transcoder: t.transcoder, hooks: hooksWrapper.Hooks(), cluster: t.cluster, queryStateLock: new(sync.Mutex), queryConfig: TransactionQueryOptions{ ScanConsistency: scanConsistency, }, logger: logger, attemptID: attemptID, } if hooksWrapper != nil { hooksWrapper.SetAttemptContext(attempt) } lambdaErr := logicFn(&attempt) if !singleQueryMode && lambdaErr != nil { logger.logInfof(attemptID, "Lambda returned error and not single query mode") var txnErr *TransactionOperationFailedError if !errors.As(lambdaErr, &txnErr) { // We wrap non-TOF errors in a TOF. lambdaErr = operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, ShouldNotRollback: false, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ErrorCause: lambdaErr, ShouldNotCommit: true, }, &attempt) } } finalErr := lambdaErr if !singleQueryMode { if attempt.canCommit() { finalErr = attempt.commit() } if attempt.shouldRollback() { rollbackErr := attempt.rollback() if rollbackErr != nil { logWarnf("rollback after error failed: %s", rollbackErr) } } } toRaise := attempt.finalErrorToRaise() if attempt.shouldRetry() && toRaise != gocbcore.TransactionErrorReasonSuccess { logDebugf("retrying lambda after backoff") sleep := backoffCalc() logger.logInfof(attemptID, "Will retry lambda after %s", sleep) time.Sleep(sleep) continue } // We don't want the TOF to be the cause in the final error we return so we unwrap it. var finalErrCause error if finalErr != nil { var txnErr *TransactionOperationFailedError if errors.As(finalErr, &txnErr) { finalErrCause = txnErr.InternalUnwrap() } else { finalErrCause = finalErr } } switch toRaise { case gocbcore.TransactionErrorReasonSuccess: if singleQueryMode && finalErr != nil { return nil, finalErr } unstagingComplete := attempt.attempt().State == TransactionAttemptStateCompleted return &TransactionResult{ TransactionID: txn.ID(), UnstagingComplete: unstagingComplete, Logs: logger.Logs(), }, nil case gocbcore.TransactionErrorReasonTransactionFailed: return nil, &TransactionFailedError{ cause: finalErrCause, result: &TransactionResult{ TransactionID: txn.ID(), UnstagingComplete: false, Logs: logger.Logs(), }, } case gocbcore.TransactionErrorReasonTransactionExpired: // If we expired during gocbcore auto-rollback then we return failed with the error cause rather // than expired. This occurs when we commit itself errors and gocbcore auto rolls back the transaction. if attempt.attempt().PreExpiryAutoRollback { return nil, &TransactionFailedError{ cause: finalErrCause, result: &TransactionResult{ TransactionID: txn.ID(), UnstagingComplete: false, Logs: logger.Logs(), }, } } return nil, &TransactionExpiredError{ result: &TransactionResult{ TransactionID: txn.ID(), UnstagingComplete: false, Logs: logger.Logs(), }, } case gocbcore.TransactionErrorReasonTransactionCommitAmbiguous: return nil, &TransactionCommitAmbiguousError{ cause: finalErrCause, result: &TransactionResult{ TransactionID: txn.ID(), UnstagingComplete: false, Logs: logger.Logs(), }, } case gocbcore.TransactionErrorReasonTransactionFailedPostCommit: return &TransactionResult{ TransactionID: txn.ID(), UnstagingComplete: false, Logs: logger.Logs(), }, nil default: return nil, errors.New("invalid final transaction state") } } } // Close will shut down this Transactions object, shutting down all // background tasks associated with it. func (t *Transactions) close() error { return t.txns.Close() } func (t *Transactions) agentProvider(bucketName string) (*gocbcore.Agent, string, error) { b := t.cluster.Bucket(bucketName) agent, err := b.Internal().IORouter() if err != nil { return nil, "", err } return agent, "", err } func (t *Transactions) atrLocationsProvider() ([]gocbcore.TransactionLostATRLocation, error) { return t.cleanupCollections, nil } func (t *Transactions) singleQuery(statement string, scope *Scope, opts QueryOptions) (*QueryResult, error) { if opts.Context != nil { return nil, makeInvalidArgumentsError("cannot use context and transactions together") } config := &TransactionOptions{ DurabilityLevel: opts.AsTransaction.DurabilityLevel, Timeout: opts.Timeout, } config.Internal.Hooks = opts.AsTransaction.Internal.Hooks var queryRes *QueryResult res, err := t.run(func(context *TransactionAttemptContext) error { // We need to tell the core loop that autocommit and autorollback are disabled. // context.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ // ShouldNotCommit: true, // ShouldNotRollback: true, // }) qRes, err := context.queryWrapper(scope, statement, opts, "query", false, false, nil, true) if err != nil { return err } queryRes = qRes // If the result contains rows then we can't immediately check for errors, so we need to return here. if len(queryRes.peekNext()) > 0 { // We consider this success so tell the core to not retry - any errors on stream will happen outside the // context of the core loop. // context.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ // ShouldNotRetry: true, // }) return nil } if err := qRes.Err(); err != nil { return queryMaybeTranslateToTransactionsError(err, context) } meta, err := qRes.MetaData() if err != nil { return queryMaybeTranslateToTransactionsError(err, context) } if meta.Status == QueryStatusFatal { return operationFailed(transactionQueryOperationFailedDef{ ShouldNotRetry: true, Reason: gocbcore.TransactionErrorReasonTransactionFailed, ShouldNotCommit: true, }, context) } // We won't do autocommit or autorollback so tell the core loop to not retry. // context.txn.UpdateState(gocbcore.TransactionUpdateStateOptions{ // ShouldNotRetry: true, // }) return nil }, config, true) if err != nil { var expiredErr *TransactionExpiredError if errors.As(err, &expiredErr) { return nil, ErrUnambiguousTimeout } return nil, err } queryRes.transactionID = res.TransactionID return queryRes, nil } // TransactionsInternal exposes internal methods that are useful for testing and/or // other forms of internal use. type TransactionsInternal struct { parent *Transactions } // Internal returns an TransactionsInternal object which can be used for specialized // internal use cases. func (t *Transactions) Internal() *TransactionsInternal { return &TransactionsInternal{ parent: t, } } // ForceCleanupQueue forces the transactions client cleanup queue to drain without waiting for expirations. func (t *TransactionsInternal) ForceCleanupQueue() []TransactionCleanupAttempt { waitCh := make(chan []gocbcore.TransactionsCleanupAttempt, 1) t.parent.txns.Internal().ForceCleanupQueue(func(attempts []gocbcore.TransactionsCleanupAttempt) { waitCh <- attempts }) coreAttempts := <-waitCh var attempts []TransactionCleanupAttempt for _, attempt := range coreAttempts { attempts = append(attempts, cleanupAttemptFromCore(attempt)) } return attempts } // CleanupQueueLength returns the current length of the client cleanup queue. func (t *TransactionsInternal) CleanupQueueLength() int32 { return t.parent.txns.Internal().CleanupQueueLength() } // ClientCleanupEnabled returns whether the client cleanup process is enabled. func (t *TransactionsInternal) ClientCleanupEnabled() bool { return t.parent.txns.Config().CleanupClientAttempts } // CleanupLocations returns the set of locations currently being watched by the lost transactions process. func (t *TransactionsInternal) CleanupLocations() []gocbcore.TransactionLostATRLocation { return t.parent.txns.Internal().CleanupLocations() } gocb-2.6.3/transactions_cleanup.go000066400000000000000000000351641441755043100172170ustar00rootroot00000000000000package gocb import ( "github.com/couchbase/gocbcore/v10" "time" ) // TransactionDocRecord represents an individual document operation requiring cleanup. // Internal: This should never be used and is not supported. type TransactionDocRecord struct { CollectionName string ScopeName string BucketName string ID string } // TransactionCleanupAttempt represents the result of running cleanup for a transaction transactionAttempt. // Internal: This should never be used and is not supported. type TransactionCleanupAttempt struct { Success bool IsReqular bool AttemptID string AtrID string AtrCollectionName string AtrScopeName string AtrBucketName string Request *TransactionCleanupRequest } // TransactionCleanupRequest represents a complete transaction transactionAttempt that requires cleanup. // Internal: This should never be used and is not supported. type TransactionCleanupRequest struct { AttemptID string AtrID string AtrCollectionName string AtrScopeName string AtrBucketName string Inserts []TransactionDocRecord Replaces []TransactionDocRecord Removes []TransactionDocRecord State TransactionAttemptState ForwardCompat map[string][]TransactionsForwardCompatibilityEntry } // TransactionsForwardCompatibilityEntry represents a forward compatibility entry. // Internal: This should never be used and is not supported. type TransactionsForwardCompatibilityEntry struct { ProtocolVersion string `json:"p,omitempty"` ProtocolExtension string `json:"e,omitempty"` Behaviour string `json:"b,omitempty"` RetryInterval int `json:"ra,omitempty"` } // TransactionsClientRecordDetails is the result of processing a client record. // Internal: This should never be used and is not supported. type TransactionsClientRecordDetails struct { NumActiveClients int IndexOfThisClient int ClientIsNew bool ExpiredClientIDs []string NumExistingClients int NumExpiredClients int OverrideEnabled bool OverrideActive bool OverrideExpiresCas int64 CasNowNanos int64 AtrsHandledByClient []string CheckAtrEveryNMillis int ClientUUID string } // TransactionsProcessATRStats is the stats recorded when running a ProcessATR request. // Internal: This should never be used and is not supported. type TransactionsProcessATRStats struct { NumEntries int NumEntriesExpired int } // TransactionsCleaner is responsible for performing cleanup of completed transactions. // Internal: This should never be used and is not supported. type TransactionsCleaner interface { AddRequest(req *TransactionCleanupRequest) bool PopRequest() *TransactionCleanupRequest ForceCleanupQueue() []TransactionCleanupAttempt QueueLength() int32 CleanupAttempt(bucket *Bucket, isRegular bool, req *TransactionCleanupRequest) TransactionCleanupAttempt Close() } // NewTransactionsCleaner returns a TransactionsCleaner implementation. // Internal: This should never be used and is not supported. func NewTransactionsCleaner(bucketProvider TransactionsBucketProviderFn, config *TransactionsConfig) TransactionsCleaner { cleanupHooksWrapper := &coreTxnsCleanupHooksWrapper{ CleanupHooks: config.Internal.CleanupHooks, } corecfg := &gocbcore.TransactionsConfig{} corecfg.DurabilityLevel = gocbcore.TransactionDurabilityLevel(config.DurabilityLevel) corecfg.Internal.Hooks = nil corecfg.CleanupQueueSize = config.CleanupConfig.CleanupQueueSize corecfg.BucketAgentProvider = func(bucketName string) (*gocbcore.Agent, string, error) { bucket, user, err := bucketProvider(bucketName) if err != nil { return nil, "", err } agent, err := bucket.Internal().IORouter() if err != nil { return nil, "", err } return agent, user, nil } corecfg.Internal.CleanUpHooks = cleanupHooksWrapper corecfg.Internal.NumATRs = config.Internal.NumATRs corecfg.KeyValueTimeout = 2500 * time.Millisecond return &coreTransactionsCleanerWrapper{ wrapped: gocbcore.NewTransactionsCleaner(corecfg), } } type coreTransactionsCleanerWrapper struct { wrapped gocbcore.TransactionsCleaner } func (ccw *coreTransactionsCleanerWrapper) AddRequest(req *TransactionCleanupRequest) bool { return ccw.wrapped.AddRequest(cleanupRequestToCore(req)) } func (ccw *coreTransactionsCleanerWrapper) PopRequest() *TransactionCleanupRequest { return cleanupRequestFromCore(ccw.wrapped.PopRequest()) } func (ccw *coreTransactionsCleanerWrapper) ForceCleanupQueue() []TransactionCleanupAttempt { waitCh := make(chan []TransactionCleanupAttempt, 1) ccw.wrapped.ForceCleanupQueue(func(coreAttempts []gocbcore.TransactionsCleanupAttempt) { var attempts []TransactionCleanupAttempt for _, attempt := range coreAttempts { attempts = append(attempts, cleanupAttemptFromCore(attempt)) } waitCh <- attempts }) return <-waitCh } func (ccw *coreTransactionsCleanerWrapper) QueueLength() int32 { return ccw.wrapped.QueueLength() } func (ccw *coreTransactionsCleanerWrapper) CleanupAttempt(bucket *Bucket, isRegular bool, req *TransactionCleanupRequest) TransactionCleanupAttempt { waitCh := make(chan TransactionCleanupAttempt, 1) a, err := bucket.Internal().IORouter() if err != nil { return TransactionCleanupAttempt{ Success: false, IsReqular: isRegular, AttemptID: req.AttemptID, AtrID: req.AtrID, AtrCollectionName: req.AtrCollectionName, AtrScopeName: req.AtrScopeName, AtrBucketName: req.AtrBucketName, Request: req, } } ccw.wrapped.CleanupAttempt(a, "", cleanupRequestToCore(req), isRegular, func(attempt gocbcore.TransactionsCleanupAttempt) { waitCh <- cleanupAttemptFromCore(attempt) }) return <-waitCh } func (ccw *coreTransactionsCleanerWrapper) Close() { ccw.wrapped.Close() } // LostTransactionsCleaner is responsible for performing cleanup of lost transactions. // Internal: This should never be used and is not supported. type LostTransactionsCleaner interface { ProcessATR(bucket *Bucket, collection, scope, atrID string) ([]TransactionCleanupAttempt, TransactionsProcessATRStats) ProcessClient(bucket *Bucket, collection, scope, clientUUID string) (*TransactionsClientRecordDetails, error) RemoveClient(uuid string) error Close() } type coreLostTransactionsCleanerWrapper struct { wrapped gocbcore.LostTransactionCleaner } func (clcw *coreLostTransactionsCleanerWrapper) ProcessATR(bucket *Bucket, collection, scope, atrID string) ([]TransactionCleanupAttempt, TransactionsProcessATRStats) { a, err := bucket.Internal().IORouter() if err != nil { return nil, TransactionsProcessATRStats{} } var ourAttempts []TransactionCleanupAttempt var ourStats TransactionsProcessATRStats waitCh := make(chan struct{}, 1) clcw.wrapped.ProcessATR(a, "", collection, scope, atrID, func(attempts []gocbcore.TransactionsCleanupAttempt, stats gocbcore.TransactionProcessATRStats, _ error) { for _, a := range attempts { ourAttempts = append(ourAttempts, cleanupAttemptFromCore(a)) } ourStats = TransactionsProcessATRStats(stats) waitCh <- struct{}{} }) <-waitCh return ourAttempts, ourStats } func (clcw *coreLostTransactionsCleanerWrapper) ProcessClient(bucket *Bucket, collection, scope, clientUUID string) (*TransactionsClientRecordDetails, error) { type result struct { recordDetails *TransactionsClientRecordDetails err error } waitCh := make(chan result, 1) a, err := bucket.Internal().IORouter() if err != nil { return nil, err } clcw.wrapped.ProcessClient(a, "", collection, scope, clientUUID, func(details *gocbcore.TransactionClientRecordDetails, err error) { if err != nil { waitCh <- result{ err: err, } return } waitCh <- result{ recordDetails: &TransactionsClientRecordDetails{ NumActiveClients: details.NumActiveClients, IndexOfThisClient: details.IndexOfThisClient, ClientIsNew: details.ClientIsNew, ExpiredClientIDs: details.ExpiredClientIDs, NumExistingClients: details.NumExistingClients, NumExpiredClients: details.NumExpiredClients, OverrideEnabled: details.OverrideEnabled, OverrideActive: details.OverrideActive, OverrideExpiresCas: details.OverrideExpiresCas, CasNowNanos: details.CasNowNanos, AtrsHandledByClient: details.AtrsHandledByClient, CheckAtrEveryNMillis: details.CheckAtrEveryNMillis, ClientUUID: details.ClientUUID, }, } }) res := <-waitCh return res.recordDetails, res.err } func (clcw *coreLostTransactionsCleanerWrapper) RemoveClient(uuid string) error { return clcw.wrapped.RemoveClientFromAllLocations(uuid) } func (clcw *coreLostTransactionsCleanerWrapper) Close() { clcw.wrapped.Close() } // TransactionsBucketProviderFn is a function used to provide a bucket for // a particular bucket by name. // Internal: This should never be used and is not supported. type TransactionsBucketProviderFn func(bucket string) (*Bucket, string, error) type TransactionsLostCleanupKeyspaceProviderFn func() ([]TransactionKeyspace, error) // NewLostTransactionsCleanup returns a LostTransactionsCleaner implementation. // Internal: This should never be used and is not supported. func NewLostTransactionsCleanup(bucketProvider TransactionsBucketProviderFn, locationProvider TransactionsLostCleanupKeyspaceProviderFn, config *TransactionsConfig) LostTransactionsCleaner { cleanupHooksWrapper := &coreTxnsClientRecordHooksWrapper{ coreTxnsCleanupHooksWrapper: coreTxnsCleanupHooksWrapper{ CleanupHooks: config.Internal.CleanupHooks, }, ClientRecordHooks: config.Internal.ClientRecordHooks, } corecfg := &gocbcore.TransactionsConfig{} corecfg.DurabilityLevel = gocbcore.TransactionDurabilityLevel(config.DurabilityLevel) corecfg.Internal.Hooks = nil corecfg.CleanupQueueSize = config.CleanupConfig.CleanupQueueSize corecfg.BucketAgentProvider = func(bucketName string) (*gocbcore.Agent, string, error) { bucket, user, err := bucketProvider(bucketName) if err != nil { return nil, "", err } agent, err := bucket.Internal().IORouter() if err != nil { return nil, "", err } return agent, user, nil } corecfg.LostCleanupATRLocationProvider = func() ([]gocbcore.TransactionLostATRLocation, error) { locations, err := locationProvider() if err != nil { return nil, err } atrLocs := make([]gocbcore.TransactionLostATRLocation, len(locations)) for i, loc := range locations { atrLocs[i] = gocbcore.TransactionLostATRLocation{ BucketName: loc.BucketName, CollectionName: loc.CollectionName, ScopeName: loc.ScopeName, } } return atrLocs, nil } corecfg.Internal.CleanUpHooks = cleanupHooksWrapper corecfg.Internal.ClientRecordHooks = cleanupHooksWrapper corecfg.Internal.NumATRs = config.Internal.NumATRs corecfg.KeyValueTimeout = 2500 * time.Millisecond return &coreLostTransactionsCleanerWrapper{ wrapped: gocbcore.NewLostTransactionCleaner(corecfg), } } func cleanupAttemptFromCore(attempt gocbcore.TransactionsCleanupAttempt) TransactionCleanupAttempt { var req *TransactionCleanupRequest if attempt.Request != nil { req = &TransactionCleanupRequest{ AttemptID: attempt.Request.AttemptID, AtrID: string(attempt.Request.AtrID), AtrCollectionName: attempt.Request.AtrCollectionName, AtrScopeName: attempt.Request.AtrScopeName, AtrBucketName: attempt.Request.AtrBucketName, Inserts: docRecordsFromCore(attempt.Request.Inserts), Replaces: docRecordsFromCore(attempt.Request.Replaces), Removes: docRecordsFromCore(attempt.Request.Removes), State: TransactionAttemptState(attempt.Request.State), } } return TransactionCleanupAttempt{ Success: attempt.Success, IsReqular: attempt.IsReqular, AttemptID: attempt.AttemptID, AtrID: string(attempt.AtrID), AtrCollectionName: attempt.AtrCollectionName, AtrScopeName: attempt.AtrScopeName, AtrBucketName: attempt.AtrBucketName, Request: req, } } func docRecordsFromCore(drs []gocbcore.TransactionsDocRecord) []TransactionDocRecord { var recs []TransactionDocRecord for _, i := range drs { recs = append(recs, TransactionDocRecord{ CollectionName: i.CollectionName, ScopeName: i.ScopeName, BucketName: i.BucketName, ID: string(i.ID), }) } return recs } func cleanupRequestFromCore(request *gocbcore.TransactionsCleanupRequest) *TransactionCleanupRequest { forwardCompat := make(map[string][]TransactionsForwardCompatibilityEntry) for k, entries := range request.ForwardCompat { if _, ok := forwardCompat[k]; !ok { forwardCompat[k] = make([]TransactionsForwardCompatibilityEntry, len(entries)) } for i, entry := range entries { forwardCompat[k][i] = TransactionsForwardCompatibilityEntry(entry) } } return &TransactionCleanupRequest{ AttemptID: request.AttemptID, AtrID: string(request.AtrID), AtrCollectionName: request.AtrCollectionName, AtrScopeName: request.AtrScopeName, AtrBucketName: request.AtrBucketName, Inserts: docRecordsFromCore(request.Inserts), Replaces: docRecordsFromCore(request.Replaces), Removes: docRecordsFromCore(request.Removes), State: TransactionAttemptState(request.State), ForwardCompat: forwardCompat, } } func cleanupRequestToCore(request *TransactionCleanupRequest) *gocbcore.TransactionsCleanupRequest { forwardCompat := make(map[string][]gocbcore.TransactionForwardCompatibilityEntry) for k, entries := range request.ForwardCompat { if _, ok := forwardCompat[k]; !ok { forwardCompat[k] = make([]gocbcore.TransactionForwardCompatibilityEntry, len(entries)) } for i, entry := range entries { forwardCompat[k][i] = gocbcore.TransactionForwardCompatibilityEntry(entry) } } return &gocbcore.TransactionsCleanupRequest{ AttemptID: request.AttemptID, AtrID: []byte(request.AtrID), AtrCollectionName: request.AtrCollectionName, AtrScopeName: request.AtrScopeName, AtrBucketName: request.AtrBucketName, Inserts: docRecordsToCore(request.Inserts), Replaces: docRecordsToCore(request.Replaces), Removes: docRecordsToCore(request.Removes), State: gocbcore.TransactionAttemptState(request.State), ForwardCompat: forwardCompat, } } func docRecordsToCore(drs []TransactionDocRecord) []gocbcore.TransactionsDocRecord { var recs []gocbcore.TransactionsDocRecord for _, i := range drs { recs = append(recs, gocbcore.TransactionsDocRecord{ CollectionName: i.CollectionName, ScopeName: i.ScopeName, BucketName: i.BucketName, ID: []byte(i.ID), }) } return recs } gocb-2.6.3/transactions_compatibility.go000066400000000000000000000010101441755043100204200ustar00rootroot00000000000000package gocb import ( "github.com/couchbase/gocbcore/v10" ) // TransactionsProtocolVersion returns the protocol version that this library supports. func TransactionsProtocolVersion() string { return gocbcore.TransactionsProtocolVersion() } // TransactionsProtocolExtensions returns a list strings representing the various features // that this specific version of the library supports within its protocol version. func TransactionsProtocolExtensions() []string { return gocbcore.TransactionsProtocolExtensions() } gocb-2.6.3/transactions_configs.go000066400000000000000000000126411441755043100172130ustar00rootroot00000000000000package gocb import ( "time" ) // TransactionsCleanupConfig specifies various tunable options related to transactions cleanup. type TransactionsCleanupConfig struct { // CleanupWindow specifies how often to the cleanup process runs // attempting to garbage collection transactions that have failed but // were not cleaned up by the previous client. CleanupWindow time.Duration // DisableClientAttemptCleanup controls where any transaction attempts made // by this client are automatically removed. DisableClientAttemptCleanup bool // DisableLostAttemptCleanup controls where a background process is created // to cleanup any ‘lost’ transaction attempts. DisableLostAttemptCleanup bool // CleanupQueueSize controls the maximum queue size for the cleanup thread. CleanupQueueSize uint32 // CleanupCollections is a set of extra collections that should be monitored // by the cleanup thread. CleanupCollections []TransactionKeyspace } // TransactionsConfig specifies various tunable options related to transactions. type TransactionsConfig struct { // MetadataCollection specifies a specific location to place meta-data. MetadataCollection *TransactionKeyspace // ExpirationTimout sets the maximum time that transactions created // by this Transactions object can run for, before expiring. Timeout time.Duration // DurabilityLevel specifies the durability level that should be used // for all write operations performed by this Transactions object. DurabilityLevel DurabilityLevel // QueryConfig specifies any query configuration to use in transactions. QueryConfig TransactionsQueryConfig // CleanupConfig specifies cleanup configuration to use in transactions. CleanupConfig TransactionsCleanupConfig // Internal specifies a set of options for internal use. // Internal: This should never be used and is not supported. Internal struct { Hooks TransactionHooks CleanupHooks TransactionCleanupHooks ClientRecordHooks TransactionClientRecordHooks NumATRs int } } // TransactionOptions specifies options which can be overridden on a per transaction basis. type TransactionOptions struct { // DurabilityLevel specifies the durability level that should be used // for all write operations performed by this transaction. DurabilityLevel DurabilityLevel // Timeout sets the maximum time that this transaction can run for, before expiring. Timeout time.Duration // MetadataCollection specifies a specific Collection to place meta-data. MetadataCollection *Collection // Internal specifies a set of options for internal use. // Internal: This should never be used and is not supported. Internal struct { Hooks TransactionHooks } } // TransactionsQueryConfig specifies various tunable query options related to transactions. type TransactionsQueryConfig struct { ScanConsistency QueryScanConsistency } // SingleQueryTransactionOptions specifies various tunable query options related to single query transactions. type SingleQueryTransactionOptions struct { DurabilityLevel DurabilityLevel // Internal specifies a set of options for internal use. // Internal: This should never be used and is not supported. Internal struct { Hooks TransactionHooks } } // TransactionKeyspace specifies a specific location where ATR entries should be // placed when performing transactions. type TransactionKeyspace struct { BucketName string ScopeName string CollectionName string } // TransactionQueryOptions specifies the set of options available when running queries as a part of a transaction. // This is a subset of QueryOptions. type TransactionQueryOptions struct { ScanConsistency QueryScanConsistency Profile QueryProfileMode // ScanCap is the maximum buffered channel size between the indexer connectionManager and the query service for index scans. ScanCap uint32 // PipelineBatch controls the number of items execution operators can batch for Fetch from the KV. PipelineBatch uint32 // PipelineCap controls the maximum number of items each execution operator can buffer between various operators. PipelineCap uint32 // ScanWait is how long the indexer is allowed to wait until it can satisfy ScanConsistency/ConsistentWith criteria. ScanWait time.Duration Readonly bool // ClientContextID provides a unique ID for this query which can be used matching up requests between connectionManager and // server. If not provided will be assigned a uuid value. ClientContextID string PositionalParameters []interface{} NamedParameters map[string]interface{} // FlexIndex tells the query engine to use a flex index (utilizing the search service). FlexIndex bool // Raw provides a way to provide extra parameters in the request body for the query. Raw map[string]interface{} Prepared bool Scope *Scope } func (qo *TransactionQueryOptions) toSDKOptions() QueryOptions { scanc := qo.ScanConsistency if scanc == 0 { scanc = QueryScanConsistencyRequestPlus } return QueryOptions{ ScanConsistency: scanc, Profile: qo.Profile, ScanCap: qo.ScanCap, PipelineBatch: qo.PipelineBatch, PipelineCap: qo.PipelineCap, ScanWait: qo.ScanWait, Readonly: qo.Readonly, ClientContextID: qo.ClientContextID, PositionalParameters: qo.PositionalParameters, NamedParameters: qo.NamedParameters, Raw: qo.Raw, Adhoc: !qo.Prepared, FlexIndex: qo.FlexIndex, } } gocb-2.6.3/transactions_constants.go000066400000000000000000000026171441755043100176010ustar00rootroot00000000000000package gocb import "github.com/couchbase/gocbcore/v10" // TransactionErrorReason is the reason why a transaction should be failed. // Internal: This should never be used and is not supported. type TransactionErrorReason uint8 const ( // TransactionErrorReasonSuccess indicates the transaction succeeded and did not fail. TransactionErrorReasonSuccess TransactionErrorReason = TransactionErrorReason(gocbcore.TransactionErrorReasonSuccess) // TransactionErrorReasonTransactionFailed indicates the transaction should be failed because it failed. TransactionErrorReasonTransactionFailed = TransactionErrorReason(gocbcore.TransactionErrorReasonTransactionFailed) // TransactionErrorReasonTransactionExpired indicates the transaction should be failed because it expired. TransactionErrorReasonTransactionExpired = TransactionErrorReason(gocbcore.TransactionErrorReasonTransactionExpired) // TransactionErrorReasonTransactionCommitAmbiguous indicates the transaction should be failed and the commit was ambiguous. TransactionErrorReasonTransactionCommitAmbiguous = TransactionErrorReason(gocbcore.TransactionErrorReasonTransactionCommitAmbiguous) // TransactionErrorReasonTransactionFailedPostCommit indicates the transaction should be failed because it failed post commit. TransactionErrorReasonTransactionFailedPostCommit = TransactionErrorReason(gocbcore.TransactionErrorReasonTransactionFailedPostCommit) ) gocb-2.6.3/transactions_query_test.go000066400000000000000000000365161441755043100177760ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" "fmt" "github.com/stretchr/testify/mock" "time" ) func (suite *IntegrationTestSuite) TestTransactionsQueryModeInsert() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryinsert" docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } _, err = ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsQueryModeReplace() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryreplace" docValue := map[string]interface{}{ "test": "test", } docValue2 := map[string]interface{}{ "test": "test2", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } _, err = ctx.Replace(getRes, docValue2) if err != nil { return err } return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue2) } func (suite *IntegrationTestSuite) TestTransactionsQueryModeRemove() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryremove" docValue := map[string]interface{}{ "test": "test", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } err = ctx.Remove(getRes) if err != nil { return err } return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocumentNotFound(docID) } func (suite *IntegrationTestSuite) TestTransactionsQueryModeDocNotFound() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "querydocnotfound" txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } _, err = ctx.Get(globalCollection, docID) if err != nil { return err } return nil }, nil) suite.Assert().ErrorIs(err, ErrDocumentNotFound) suite.Assert().Nil(txnRes) } func (suite *IntegrationTestSuite) TestTransactionsQueryModeDocFound() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "querydocfound" docValue := map[string]interface{}{ "test": "test", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocContent map[string]interface{} err = getRes.Content(&actualDocContent) if err != nil { return err } suite.Assert().Equal(docValue, actualDocContent) return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsQueryUpdateStatement() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryupdatestatement" docValue := map[string]interface{}{ "test": "test", } resultDocValue := map[string]interface{}{ "test": "test", "foo": float64(2), } err := globalCluster.QueryIndexes().CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ CollectionName: globalCollection.Name(), ScopeName: globalScope.Name(), IgnoreIfExists: true, }) suite.Require().NoError(err) _, err = globalCollection.Upsert(docID, docValue, &UpsertOptions{ DurabilityLevel: DurabilityLevelMajority, }) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { queryRes, err := ctx.Query( fmt.Sprintf("UPDATE `%s` SET foo = 2 WHERE META().id = \"%s\"", globalCollection.Name(), docID, ), &TransactionQueryOptions{ Scope: globalScope, }, ) if err != nil { return err } meta, err := queryRes.MetaData() if err != nil { return err } suite.Assert().Equal(uint64(1), meta.Metrics.MutationCount) getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocContent map[string]interface{} err = getRes.Content(&actualDocContent) if err != nil { return err } suite.Assert().Equal(resultDocValue, actualDocContent) return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, resultDocValue) } func (suite *IntegrationTestSuite) TestTransactionsQueryUpdateStatementKVReplace() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryupdatestatementkvreplace" docValue := map[string]interface{}{ "test": "test", } docValue2 := map[string]interface{}{ "test": "test", "foo": float64(4), } resultDocValue := map[string]interface{}{ "test": "test", "foo": float64(4), } err := globalCluster.QueryIndexes().CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ CollectionName: globalCollection.Name(), ScopeName: globalScope.Name(), IgnoreIfExists: true, }) suite.Require().NoError(err) _, err = globalCollection.Upsert(docID, docValue, &UpsertOptions{ DurabilityLevel: DurabilityLevelMajority, }) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query( fmt.Sprintf("UPDATE `%s` SET foo = 2 WHERE META().id = \"%s\"", globalCollection.Name(), docID, ), &TransactionQueryOptions{ Scope: globalScope, }, ) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } _, err = ctx.Replace(getRes, docValue2) if err != nil { return err } return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, resultDocValue) } func (suite *IntegrationTestSuite) TestTransactionsQueryUpdateStatementKVRemove() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryupdatestatementkvremove" docValue := map[string]interface{}{ "test": "test", } err := globalCluster.QueryIndexes().CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ CollectionName: globalCollection.Name(), ScopeName: globalScope.Name(), IgnoreIfExists: true, }) suite.Require().NoError(err) _, err = globalCollection.Upsert(docID, docValue, &UpsertOptions{ DurabilityLevel: DurabilityLevelMajority, }) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query( fmt.Sprintf("UPDATE `%s` SET foo = 2 WHERE META().id = \"%s\"", globalCollection.Name(), docID, ), &TransactionQueryOptions{ Scope: globalScope, }, ) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } err = ctx.Remove(getRes) if err != nil { return err } return nil }, nil) suite.Require().NoError(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocumentNotFound(docID) } func (suite *IntegrationTestSuite) TestTransactionsQueryDoubleInsertStatement() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "querydoubleinsert" err := globalCluster.QueryIndexes().CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ CollectionName: globalCollection.Name(), ScopeName: globalScope.Name(), IgnoreIfExists: true, }) suite.Require().NoError(err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query( fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {\"foo\": 2})", globalBucket.Name(), docID, ), nil) if err != nil { return err } _, err = ctx.Query( fmt.Sprintf("INSERT INTO `%s` VALUES (\"%s\", {\"foo\": 2})", globalBucket.Name(), docID, ), nil) if err != nil { return err } return nil }, nil) suite.Assert().ErrorIs(err, ErrDocumentExists) suite.Assert().Nil(txnRes) } func (suite *IntegrationTestSuite) TestTransactionsInsertReadByQuery() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "insertreadbyquery" docValue := map[string]interface{}{ "test": "test", } err := globalCluster.QueryIndexes().CreatePrimaryIndex(globalBucket.Name(), &CreatePrimaryQueryIndexOptions{ CollectionName: globalCollection.Name(), ScopeName: globalScope.Name(), IgnoreIfExists: true, }) suite.Require().NoError(err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err = ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } queryRes, err := ctx.Query(fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE META().id = '%s'", globalCollection.Name(), globalCollection.Name(), docID), &TransactionQueryOptions{ ScanConsistency: QueryScanConsistencyRequestPlus, Scope: globalScope, }) if err != nil { return err } var actualDocValue map[string]interface{} err = queryRes.One(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) queryRes, err = ctx.Query(fmt.Sprintf("SELECT `%s`.* FROM `%s` WHERE META().id = 'insertreadbyquery'", globalCollection.Name(), globalCollection.Name()), &TransactionQueryOptions{ Scope: globalScope, }) if err != nil { return err } var vals []map[string]interface{} for queryRes.Next() { var actualDocValue map[string]interface{} err = queryRes.Row(&actualDocValue) if err != nil { return err } vals = append(vals, actualDocValue) } if suite.Assert().Len(vals, 1) { suite.Assert().Equal(docValue, vals[0]) } return nil }, &TransactionOptions{ Timeout: 30 * time.Second, }) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsQueryInsertDocExists() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsQueryFeature) suite.skipIfUnsupported(ClusterLevelQueryFeature) docID := "queryinsertdocexists" docValue := map[string]interface{}{ "test": "test", } _, err := globalCollection.Upsert(docID, "{}", nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } _, err = ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } return nil }, nil) suite.Assert().ErrorIs(err, ErrDocumentExists) suite.Assert().Nil(txnRes) } type transactionMockQueryRowReader struct { Dataset []testBreweryDocument mockQueryRowReaderBase } func (suite *UnitTestSuite) TestTransactionsQueryGocbcoreCauseError() { var dataset struct { jsonQueryResponse Errors json.RawMessage } err := loadJSONTestDataset("transaction_gocbcore_cause_error", &dataset) suite.Require().Nil(err, err) var beginWorkDataset struct { jsonQueryResponse } err = loadJSONTestDataset("transaction_begin_work_response", &beginWorkDataset) suite.Require().Nil(err, err) reader := &mockQueryRowReader{ Dataset: []testBreweryDocument{}, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(dataset.jsonQueryResponse), Suite: suite, RowsErr: &QueryError{ ErrorText: string(dataset.Errors), HTTPStatusCode: 200, Statement: "somethiung", InnerError: errors.New("query error"), Errors: []QueryErrorDesc{ { Code: 1234, Message: "1234", }, }, }, }, } beginWorkReader := &mockQueryRowReader{ Dataset: []testBreweryDocument{}, mockQueryRowReaderBase: mockQueryRowReaderBase{ Meta: suite.mustConvertToBytes(beginWorkDataset.jsonQueryResponse), }, } queryProvider := new(mockQueryProvider) // BEGIN WORK queryProvider. On("N1QLQuery", nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Return(beginWorkReader, nil). Once() // QUERY queryProvider. On("N1QLQuery", nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Return(reader, nil). Once() // ROLLBACK queryProvider. On("N1QLQuery", nil, mock.AnythingOfType("gocbcore.N1QLQueryOptions")). Return(beginWorkReader, nil). Once() cli := new(mockConnectionManager) cli.On("getQueryProvider").Return(queryProvider, nil) cli.On("close").Return(nil) cluster := suite.newCluster(cli) cluster.transactions, err = cluster.initTransactions(TransactionsConfig{ CleanupConfig: TransactionsCleanupConfig{ DisableLostAttemptCleanup: true, }, }) suite.Require().Nil(err, err) txns := cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Query("SELECT 1=1", nil) if err != nil { return err } return nil }, nil) suite.Require().ErrorIs(err, ErrAttemptExpired) var finalErr *TransactionExpiredError suite.Assert().True(errors.As(err, &finalErr)) suite.Assert().Nil(txnRes) err = cluster.Close(nil) suite.Require().Nil(err, err) } gocb-2.6.3/transactions_test.go000066400000000000000000000753631441755043100165540ustar00rootroot00000000000000package gocb import ( "errors" "fmt" "log" "time" "github.com/couchbase/gocbcore/v10" "github.com/google/uuid" ) func (suite *IntegrationTestSuite) verifyDocument(key string, val interface{}) { res, err := globalCollection.Get(key, nil) suite.Require().Nil(err, err) var actualVal interface{} err = res.Content(&actualVal) suite.Require().Nil(err, err) suite.Assert().Equal(actualVal, val) } func (suite *IntegrationTestSuite) verifyDocumentNotFound(key string) { _, err := globalCollection.Get(key, nil) suite.Require().ErrorIs(err, ErrDocumentNotFound) } func (suite *IntegrationTestSuite) TestTransactionsDoubleInsert() { suite.skipIfUnsupported(TransactionsFeature) docID := "txninsert" docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } _, err = ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } return nil }, nil) suite.Assert().Nil(txnRes) suite.Assert().ErrorIs(err, ErrDocumentExists) var txnErr *TransactionFailedError if suite.Assert().ErrorAs(err, &txnErr) { suite.Assert().NotNil(txnErr.Result()) } } func (suite *IntegrationTestSuite) TestTransactionsInsert() { suite.skipIfUnsupported(TransactionsFeature) docID := generateDocId("txninsert") docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsCustomMetadata() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfServerVersionEquals(srvVer750) metaCollectionName := "txnsCustomMetadata" collections := globalBucket.Collections() err := collections.CreateCollection(CollectionSpec{ Name: metaCollectionName, ScopeName: globalScope.Name(), }, nil) suite.Require().Nil(err, err) defer collections.DropCollection(CollectionSpec{ Name: metaCollectionName, ScopeName: globalScope.Name(), }, nil) suite.mustWaitForCollections(globalScope.Name(), []string{metaCollectionName}) tConfig := globalCluster.transactionsConfig tConfig.MetadataCollection = &TransactionKeyspace{ BucketName: globalBucket.Name(), ScopeName: globalScope.Name(), CollectionName: metaCollectionName, } c, err := Connect(globalConfig.connstr, ClusterOptions{ Authenticator: PasswordAuthenticator{ Username: globalConfig.User, Password: globalConfig.Password, }, TransactionsConfig: tConfig, }) suite.Require().Nil(err, err) defer c.Close(nil) docID := "txnsCustomMetadata" docValue := map[string]interface{}{ "test": "test", } txns := c.Transactions() var atr string txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) atr = string(ctx.txn.Attempt().AtrID) return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) res, err := globalScope.Collection(metaCollectionName).LookupIn(atr, []LookupInSpec{ GetSpec("", nil), }, &LookupInOptions{ Internal: struct { DocFlags SubdocDocFlag User string }{DocFlags: SubdocDocFlagAccessDeleted}, }) suite.Require().Nil(err, err) suite.Assert().True(res.Exists(0)) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsCustomMetadataTransactionOption() { suite.skipIfUnsupported(TransactionsFeature) metaCollectionName := generateDocId("txnsCustomMetadataTxnOption") collections := globalBucket.Collections() err := collections.CreateCollection(CollectionSpec{ Name: metaCollectionName, ScopeName: globalScope.Name(), }, nil) suite.Require().Nil(err, err) defer collections.DropCollection(CollectionSpec{ Name: metaCollectionName, ScopeName: globalScope.Name(), }, nil) suite.mustWaitForCollections(globalScope.Name(), []string{metaCollectionName}) perConfig := &TransactionOptions{ MetadataCollection: globalBucket.Collection(metaCollectionName), } docID := generateDocId("txnsCustomMetadataTxnOption") docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Transactions() var atr string txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) atr = string(ctx.txn.Attempt().AtrID) return nil }, perConfig) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) res, err := globalScope.Collection(metaCollectionName).LookupIn(atr, []LookupInSpec{ GetSpec("", nil), }, &LookupInOptions{ Internal: struct { DocFlags SubdocDocFlag User string }{DocFlags: SubdocDocFlagAccessDeleted}, }) suite.Require().Nil(err, err) suite.Assert().True(res.Exists(0)) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsCustomMetadataLocationRemoved() { suite.skipIfUnsupported(TransactionsFeature) suite.skipIfUnsupported(TransactionsRemoveLocationFeature) metaCollectionName := uuid.NewString() collections := globalBucket.Collections() err := collections.CreateCollection(CollectionSpec{ Name: metaCollectionName, ScopeName: globalScope.Name(), }, nil) suite.Require().Nil(err, err) suite.mustWaitForCollections(globalScope.Name(), []string{metaCollectionName}) perConfig := &TransactionOptions{ MetadataCollection: globalBucket.Collection(metaCollectionName), } docID := uuid.NewString() docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } return nil }, perConfig) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) location := gocbcore.TransactionLostATRLocation{ BucketName: globalBucket.Name(), ScopeName: globalScope.Name(), CollectionName: metaCollectionName, } suite.Require().Contains(txns.Internal().CleanupLocations(), location) err = collections.DropCollection(CollectionSpec{ Name: metaCollectionName, ScopeName: globalScope.Name(), }, nil) suite.Require().Nil(err, err) suite.Eventually(func() bool { locations := txns.Internal().CleanupLocations() for _, loc := range locations { if loc == location { return false } } return true }, globalCluster.txnCleanupTimeout(), 100*time.Millisecond) } func (suite *IntegrationTestSuite) TestTransactionsRollback() { suite.skipIfUnsupported(TransactionsFeature) docID := "txnreplace" docValue := map[string]interface{}{ "test": "test", } docValue2 := map[string]interface{}{ "test": "test2", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { _, err := ctx.Insert(globalCollection, docID, docValue2) if err != nil { return err } getRes2, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue2 map[string]interface{} err = getRes2.Content(&actualDocValue2) if err != nil { return err } suite.Assert().Equal(docValue2, actualDocValue2) return errors.New("bail me out") }, nil) suite.Require().NotNil(err) suite.Require().Nil(txnRes) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsReadExternalToTxn() { suite.skipIfUnsupported(TransactionsFeature) docID := "txnreplace" docValue := map[string]interface{}{ "test": "test", } docValue2 := map[string]interface{}{ "test": "test2", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() interceptCh := make(chan struct{}) go func() { txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) _, err = ctx.Replace(getRes, docValue2) if err != nil { return err } interceptCh <- struct{}{} <-interceptCh getRes2, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue2 map[string]interface{} err = getRes2.Content(&actualDocValue2) if err != nil { return err } suite.Assert().Equal(docValue2, actualDocValue2) return nil }, nil) suite.Assert().Nil(err, err) suite.Assert().NotNil(txnRes) interceptCh <- struct{}{} }() <-interceptCh suite.verifyDocument(docID, docValue) interceptCh <- struct{}{} <-interceptCh suite.verifyDocument(docID, docValue2) } func (suite *IntegrationTestSuite) TestTransactionsReplace() { suite.skipIfUnsupported(TransactionsFeature) docID := "txnreplace" docValue := map[string]interface{}{ "test": "test", } docValue2 := map[string]interface{}{ "test": "test2", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) _, err = ctx.Replace(getRes, docValue2) if err != nil { return err } getRes2, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue2 map[string]interface{} err = getRes2.Content(&actualDocValue2) if err != nil { return err } suite.Assert().Equal(docValue2, actualDocValue2) return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue2) } func (suite *IntegrationTestSuite) TestTransactionsRemove() { suite.skipIfUnsupported(TransactionsFeature) docID := "txnremove" docValue := map[string]interface{}{ "test": "test", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) err = ctx.Remove(getRes) if err != nil { return err } _, err = ctx.Get(globalCollection, docID) if !errors.Is(err, ErrDocumentNotFound) { return errors.New("get should have returned a doc not found") } return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) } func (suite *IntegrationTestSuite) TestTransactionsInsertReplace() { suite.skipIfUnsupported(TransactionsFeature) docID := generateDocId("txninsertreplace") docValue := map[string]interface{}{ "test": "test", } docValue2 := map[string]interface{}{ "test": "test2", } txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { res, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } _, err = ctx.Replace(res, docValue2) if err != nil { return err } getRes, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue2, actualDocValue) return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue2) } func (suite *IntegrationTestSuite) TestTransactionsInsertRemove() { suite.skipIfUnsupported(TransactionsFeature) docID := "txninsertremove" docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { res, err := ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } err = ctx.Remove(res) if err != nil { return err } _, err = ctx.Get(globalCollection, docID) if !errors.Is(err, ErrDocumentNotFound) { return errors.New(fmt.Sprintf("error should have been doc not found, was %#v", err)) } return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocumentNotFound(docID) } func (suite *IntegrationTestSuite) TestTransactionsUserError() { suite.skipIfUnsupported(TransactionsFeature) var ErrOopsieDoodle = errors.New("im an error") txns := globalCluster.Cluster.Transactions() _, err := txns.Run(func(ctx *TransactionAttemptContext) error { return ErrOopsieDoodle }, nil) suite.Require().ErrorIs(err, ErrOopsieDoodle) } func (suite *IntegrationTestSuite) TestTransactionsGetDocNotFoundAllowsContinue() { suite.skipIfUnsupported(TransactionsFeature) docID := generateDocId("txndocnotfoundallowscontinue") docValue := map[string]interface{}{ "test": "test", } txns := globalCluster.Cluster.Transactions() txnRes, err := txns.Run(func(ctx *TransactionAttemptContext) error { getRes, err := ctx.Get(globalCollection, docID) if !errors.Is(err, ErrDocumentNotFound) { return fmt.Errorf("get should have returned document not found but was %v", err) } _, err = ctx.Insert(globalCollection, docID, docValue) if err != nil { return err } getRes, err = ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = getRes.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) return nil }, nil) suite.Require().Nil(err, err) suite.Assert().True(txnRes.UnstagingComplete) suite.Assert().NotEmpty(txnRes.TransactionID) suite.verifyDocument(docID, docValue) } func (suite *IntegrationTestSuite) TestTransactionsGetOnly() { suite.skipIfUnsupported(TransactionsFeature) docID := "getOnly" docValue := map[string]interface{}{ "test": "test", } _, err := globalCollection.Upsert(docID, docValue, nil) suite.Require().Nil(err, err) txns := globalCluster.Cluster.Transactions() _, err = txns.Run(func(ctx *TransactionAttemptContext) error { res, err := ctx.Get(globalCollection, docID) if err != nil { return err } var actualDocValue map[string]interface{} err = res.Content(&actualDocValue) if err != nil { return err } suite.Assert().Equal(docValue, actualDocValue) return nil }, nil) suite.Require().Nil(err, err) } func (suite *IntegrationTestSuite) TestMultipleTransactionObjects() { cli := new(mockConnectionManager) cli.On("close").Return(nil) tConfig := TransactionsConfig{} tConfig.CleanupConfig.DisableLostAttemptCleanup = true c := clusterFromOptions(ClusterOptions{ Tracer: &NoopTracer{}, Meter: &NoopMeter{}, TransactionsConfig: tConfig, }) defer c.Close(nil) c.connectionManager = cli txns := c.Transactions() txns2 := c.Transactions() suite.Assert().Equal(&txns, &txns2) } func (suite *UnitTestSuite) TestTransactionsCustomMetadataAddedToCleanupLocs() { metaCollectionName := "TestTransactionsCustomMetadataAddedToCleanupLocs" cli := new(mockConnectionManager) cli.On("openBucket", "default").Return(nil) cli.On("openBucket", "connect").Return(nil) cli.On("connection", "default").Return(&gocbcore.Agent{}, nil) cli.On("close").Return(nil) tConfig := TransactionsConfig{} tConfig.MetadataCollection = &TransactionKeyspace{ BucketName: "default", ScopeName: "_default", CollectionName: metaCollectionName, } tConfig.CleanupConfig.DisableLostAttemptCleanup = true tConfig.CleanupConfig.DisableClientAttemptCleanup = true c := clusterFromOptions(ClusterOptions{ Tracer: &NoopTracer{}, Meter: &NoopMeter{}, TransactionsConfig: tConfig, }) defer c.Close(nil) c.connectionManager = cli txns, err := c.initTransactions(tConfig) suite.Require().Nil(err, err) locs, err := txns.atrLocationsProvider() suite.Require().Nil(err) suite.Require().Len(locs, 1) suite.Require().Contains(locs, gocbcore.TransactionLostATRLocation{ BucketName: "default", ScopeName: "_default", CollectionName: metaCollectionName, }) } func (suite *UnitTestSuite) TestTransactionsCustomMetadataAlreadyInCleanupCollections() { metaCollectionName := "TestTransactionsCustomMetadataAlreadyInCleanupCollections" cli := new(mockConnectionManager) cli.On("openBucket", "default").Return(nil) cli.On("connection", "default").Return(&gocbcore.Agent{}, nil) cli.On("close").Return(nil) tConfig := TransactionsConfig{} tConfig.MetadataCollection = &TransactionKeyspace{ BucketName: "default", ScopeName: "_default", CollectionName: metaCollectionName, } tConfig.CleanupConfig.CleanupCollections = []TransactionKeyspace{ { BucketName: "default", ScopeName: "_default", CollectionName: metaCollectionName, }, } tConfig.CleanupConfig.DisableLostAttemptCleanup = true tConfig.CleanupConfig.DisableClientAttemptCleanup = true c := clusterFromOptions(ClusterOptions{ Tracer: &NoopTracer{}, Meter: &NoopMeter{}, TransactionsConfig: tConfig, }) defer c.Close(nil) c.connectionManager = cli txns, err := c.initTransactions(tConfig) suite.Require().Nil(err, err) locs, err := txns.atrLocationsProvider() suite.Require().Nil(err) suite.Require().Len(locs, 1) suite.Require().Contains(locs, gocbcore.TransactionLostATRLocation{ BucketName: "default", ScopeName: "_default", CollectionName: metaCollectionName, }) } func (suite *IntegrationTestSuite) TestTransactionsNoContentionSingleThreadPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsNoContentionMedTxnSingleThreadPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k000tok099", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsNoContentionBigTxnSingleThreadPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k000tok499", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowLateContentionTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19andkC", true, 50}, {"k20tok39andkC", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowLateContentionTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19andkC", false, 50}, {"k20tok39andkC", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowEarlyContentionTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"kCandk00tok19", true, 50}, {"kCandk20tok39", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowEarlyContentionTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"kCandk00tok19", false, 50}, {"kCandk20tok39", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsHighContentionThreeThreadsTwoPessimisticOneOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsHighContentionThreeThreadsTwoOptimisticOnePessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", false, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsHighContentionTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", true, 50}, {"k00tok19", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsHighContentionTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", false, 50}, {"k00tok19", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsVHighContentionTenThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, {"k00tok19", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsVHighContentionTenThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, {"k00tok19", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowLateContentionMedTxnTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k000tok099andkC", true, 50}, {"k100tok199andkC", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowLateContentionMedTxnTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k000tok099andkC", false, 50}, {"k100tok199andkC", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowEarlyContentionMedTxnTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"kCandk000tok099", true, 50}, {"kCandk100tok199", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsLowEarlyContentionMedTxnTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"kCandk000tok099", false, 50}, {"kCandk100tok199", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsOneSidedEarlyContentionMedTxnTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"kCON", true, 50 * 101}, {"kCandk100tok199", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsOneSidedEarlyContentionMedTxnTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"kCON", false, 50 * 101}, {"kCandk100tok199", false, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsHighContentionBigTxnTwoThreadsOptimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok199", true, 50}, {"k00tok199", true, 50}, }) } func (suite *IntegrationTestSuite) TestTransactionsHighContentionBigTxnTwoThreadsPessimistic() { suite.skipIfUnsupported(TransactionsBulkFeature) suite.runTranasctionLoadTest([]transactionTestGroup{ {"k00tok199", false, 50}, {"k00tok199", false, 50}, }) } type transactionTestGroup struct { name string isOptim bool numIters int } type tranactionTestResult struct { NumSuccess int NumError int NumIters int Keys []string MinTime time.Duration MaxTime time.Duration AvgTime time.Duration SumTime time.Duration } func (suite *IntegrationTestSuite) doTransactionOps(name string, keys []string, numIters int, useOptim bool) *tranactionTestResult { transactions := globalCluster.Transactions() // log.Printf(" %s testing (%+v)", name, keys) var minTime time.Duration var maxTime time.Duration var sumTime time.Duration numSuccess := 0 numError := 0 numToRun := numIters for runIdx := 0; runIdx < numToRun; runIdx++ { numIters = runIdx + 1 tstime := time.Now() _, err := transactions.Run(func(ctx *TransactionAttemptContext) error { if useOptim { resObjs := make([]*TransactionGetResult, len(keys)) valDatas := make([]map[string]int, len(keys)) for kIdx, k := range keys { resObj, err := ctx.Get(globalCollection, k) if err != nil { return err } resObjs[kIdx] = resObj } for kIdx := range keys { var valData map[string]int err := resObjs[kIdx].Content(&valData) if err != nil { return err } valData["i"]++ valDatas[kIdx] = valData } for kIdx := range keys { _, err := ctx.Replace(resObjs[kIdx], valDatas[kIdx]) if err != nil { return err } } } else { for _, k := range keys { resObj, err := ctx.Get(globalCollection, k) if err != nil { return err } var valData map[string]int err = resObj.Content(&valData) if err != nil { return err } valData["i"]++ _, err = ctx.Replace(resObj, valData) if err != nil { return err } } } return nil }, nil) if err != nil { log.Printf("run failed: %s", err) numError++ } else { numSuccess++ tetime := time.Now() tdtime := tetime.Sub(tstime) if minTime == 0 || tdtime < minTime { minTime = tdtime } if maxTime == 0 || tdtime > maxTime { maxTime = tdtime } sumTime += tdtime } } var avgTime time.Duration if suite.Assert().GreaterOrEqual(numSuccess, 1) { avgTime = sumTime / time.Duration(numSuccess) } log.Printf(" %s testing took %s, (%d iters, %d keys, %.2f success rate, min:%dms max:%dms avg:%dms)", name, sumTime.String(), numIters, len(keys), float64(numSuccess)/float64(numIters)*100, minTime/time.Millisecond, maxTime/time.Millisecond, avgTime/time.Millisecond) return &tranactionTestResult{ NumSuccess: numSuccess, NumError: numError, NumIters: numIters, Keys: keys, MinTime: minTime, MaxTime: maxTime, SumTime: sumTime, AvgTime: avgTime, } } func (suite *IntegrationTestSuite) transactionPrepDocs(allKeys []string) { testDummy := map[string]int{"i": 1} // Flush and wait for it to finish... _, err := globalCollection.Upsert("flush-watch", nil, nil) suite.Require().Nil(err, err) err = globalCluster.Buckets().FlushBucket("default", nil) suite.Require().Nil(err, err) suite.tryTimes(512, 100*time.Millisecond, func() bool { _, err := globalCollection.Get("flush-watch", nil) return errors.Is(err, ErrDocumentNotFound) }) for _, k := range allKeys { _, err := globalCollection.Insert(k, testDummy, nil) suite.Require().Nil(err, err) } } func (suite *IntegrationTestSuite) runTranasctionLoadTest(grps []transactionTestGroup) { allKeysMap := make(map[string]int) for _, grp := range grps { for _, key := range transactionsTestKeys[grp.name] { allKeysMap[key]++ } } allKeys := make([]string, 0, len(allKeysMap)) for key := range allKeysMap { allKeys = append(allKeys, key) } suite.transactionPrepDocs(allKeys) resultCh := make(chan *tranactionTestResult, 100) for grpIdx, grp := range grps { var gname string if grp.isOptim { gname = fmt.Sprintf("%d-%s-opti", grpIdx+1, grp.name) } else { gname = fmt.Sprintf("%d-%s-pess", grpIdx+1, grp.name) } go func(grp transactionTestGroup) { res := suite.doTransactionOps(gname, transactionsTestKeys[grp.name], grp.numIters, grp.isOptim) resultCh <- res }(grp) } var ttlSuccess int var ttlError int var ttlIters int var ttlWrites int var minTime time.Duration var maxTime time.Duration var sumTime time.Duration var ttlTime time.Duration groupVals := make(map[string]int) for range grps { tRes := <-resultCh if minTime == 0 || tRes.MinTime < minTime { minTime = tRes.MinTime } if maxTime == 0 || tRes.MaxTime > maxTime { maxTime = tRes.MaxTime } ttlWrites += tRes.NumSuccess * len(tRes.Keys) ttlIters += tRes.NumIters ttlSuccess += tRes.NumSuccess ttlError += tRes.NumError sumTime += tRes.SumTime if ttlTime == 0 || tRes.SumTime > ttlTime { ttlTime = tRes.SumTime } for _, key := range tRes.Keys { groupVals[key] += tRes.NumSuccess } } avgTime := sumTime / time.Duration(ttlSuccess) wps := float64(ttlWrites) / (float64(sumTime) / float64(time.Second)) log.Printf(" overall took %s, %.2f success rate, min:%dms max:%dms avg:%dms, %.2f wps", ttlTime.String(), float64(ttlSuccess)/float64(ttlIters)*100, minTime/time.Millisecond, maxTime/time.Millisecond, avgTime/time.Millisecond, wps) suite.Assert().Zero(ttlError) // VALIDATE for key, val := range groupVals { doc, err := globalCollection.Get(key, nil) if err != nil { panic(err) } var docContent map[string]int if suite.Assert().Nil(doc.Content(&docContent)) { suite.Assert().Equalf(val+1, docContent["i"], "%s - expected map[i:%d] does not match actual %+v", key, val+1, docContent) } } } gocb-2.6.3/transcoding.go000066400000000000000000000303431441755043100153050ustar00rootroot00000000000000package gocb import ( "encoding/json" "errors" gocbcore "github.com/couchbase/gocbcore/v10" ) // Transcoder provides an interface for transforming Go values to and // from raw bytes for storage and retreival from Couchbase data storage. type Transcoder interface { // Decodes retrieved bytes into a Go type. Decode([]byte, uint32, interface{}) error // Encodes a Go type into bytes for storage. Encode(interface{}) ([]byte, uint32, error) } // JSONTranscoder implements the default transcoding behavior and applies JSON transcoding to all values. // // This will apply the following behavior to the value: // binary ([]byte) -> error. // default -> JSON value, JSON Flags. type JSONTranscoder struct { } // NewJSONTranscoder returns a new JSONTranscoder. func NewJSONTranscoder() *JSONTranscoder { return &JSONTranscoder{} } // Decode applies JSON transcoding behaviour to decode into a Go type. func (t *JSONTranscoder) Decode(bytes []byte, flags uint32, out interface{}) error { valueType, compression := gocbcore.DecodeCommonFlags(flags) // Make sure compression is disabled if compression != gocbcore.NoCompression { return errors.New("unexpected value compression") } // Normal types of decoding if valueType == gocbcore.BinaryType { return errors.New("binary datatype is not supported by JSONTranscoder") } else if valueType == gocbcore.StringType { return errors.New("string datatype is not supported by JSONTranscoder") } else if valueType == gocbcore.JSONType { err := json.Unmarshal(bytes, &out) if err != nil { return err } return nil } return errors.New("unexpected expectedFlags value") } // Encode applies JSON transcoding behaviour to encode a Go type. func (t *JSONTranscoder) Encode(value interface{}) ([]byte, uint32, error) { var bytes []byte var flags uint32 var err error switch typeValue := value.(type) { case []byte: return nil, 0, errors.New("binary data is not supported by JSONTranscoder") case *[]byte: return nil, 0, errors.New("binary data is not supported by JSONTranscoder") case json.RawMessage: bytes = typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *json.RawMessage: bytes = *typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *interface{}: return t.Encode(*typeValue) default: bytes, err = json.Marshal(value) if err != nil { return nil, 0, err } flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) } // No compression supported currently return bytes, flags, nil } // RawJSONTranscoder implements passthrough behavior of JSON data. This transcoder does not apply any serialization. // It will forward data across the network without incurring unnecessary parsing costs. // // This will apply the following behavior to the value: // binary ([]byte) -> JSON bytes, JSON expectedFlags. // string -> JSON bytes, JSON expectedFlags. // default -> error. type RawJSONTranscoder struct { } // NewRawJSONTranscoder returns a new RawJSONTranscoder. func NewRawJSONTranscoder() *RawJSONTranscoder { return &RawJSONTranscoder{} } // Decode applies raw JSON transcoding behaviour to decode into a Go type. func (t *RawJSONTranscoder) Decode(bytes []byte, flags uint32, out interface{}) error { valueType, compression := gocbcore.DecodeCommonFlags(flags) // Make sure compression is disabled if compression != gocbcore.NoCompression { return errors.New("unexpected value compression") } // Normal types of decoding if valueType == gocbcore.BinaryType { return errors.New("binary datatype is not supported by RawJSONTranscoder") } else if valueType == gocbcore.StringType { return errors.New("string datatype is not supported by RawJSONTranscoder") } else if valueType == gocbcore.JSONType { switch typedOut := out.(type) { case *[]byte: *typedOut = bytes return nil case *string: *typedOut = string(bytes) return nil default: return errors.New("you must encode raw JSON data in a byte array or string") } } return errors.New("unexpected expectedFlags value") } // Encode applies raw JSON transcoding behaviour to encode a Go type. func (t *RawJSONTranscoder) Encode(value interface{}) ([]byte, uint32, error) { var bytes []byte var flags uint32 switch typeValue := value.(type) { case []byte: bytes = typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *[]byte: bytes = *typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case string: bytes = []byte(typeValue) flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *string: bytes = []byte(*typeValue) flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case json.RawMessage: bytes = typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *json.RawMessage: bytes = *typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *interface{}: return t.Encode(*typeValue) default: return nil, 0, makeInvalidArgumentsError("only binary and string data is supported by RawJSONTranscoder") } // No compression supported currently return bytes, flags, nil } // RawStringTranscoder implements passthrough behavior of raw string data. This transcoder does not apply any serialization. // // This will apply the following behavior to the value: // string -> string bytes, string expectedFlags. // default -> error. type RawStringTranscoder struct { } // NewRawStringTranscoder returns a new RawStringTranscoder. func NewRawStringTranscoder() *RawStringTranscoder { return &RawStringTranscoder{} } // Decode applies raw string transcoding behaviour to decode into a Go type. func (t *RawStringTranscoder) Decode(bytes []byte, flags uint32, out interface{}) error { valueType, compression := gocbcore.DecodeCommonFlags(flags) // Make sure compression is disabled if compression != gocbcore.NoCompression { return errors.New("unexpected value compression") } // Normal types of decoding if valueType == gocbcore.BinaryType { return errors.New("only string datatype is supported by RawStringTranscoder") } else if valueType == gocbcore.StringType { switch typedOut := out.(type) { case *string: *typedOut = string(bytes) return nil case *interface{}: *typedOut = string(bytes) return nil default: return errors.New("you must encode a string in a string or interface") } } else if valueType == gocbcore.JSONType { return errors.New("only string datatype is supported by RawStringTranscoder") } return errors.New("unexpected expectedFlags value") } // Encode applies raw string transcoding behaviour to encode a Go type. func (t *RawStringTranscoder) Encode(value interface{}) ([]byte, uint32, error) { var bytes []byte var flags uint32 switch typeValue := value.(type) { case string: bytes = []byte(typeValue) flags = gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression) case *string: bytes = []byte(*typeValue) flags = gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression) case *interface{}: return t.Encode(*typeValue) default: return nil, 0, makeInvalidArgumentsError("only raw string data is supported by RawStringTranscoder") } // No compression supported currently return bytes, flags, nil } // RawBinaryTranscoder implements passthrough behavior of raw binary data. This transcoder does not apply any serialization. // // This will apply the following behavior to the value: // binary ([]byte) -> binary bytes, binary expectedFlags. // default -> error. type RawBinaryTranscoder struct { } // NewRawBinaryTranscoder returns a new RawBinaryTranscoder. func NewRawBinaryTranscoder() *RawBinaryTranscoder { return &RawBinaryTranscoder{} } // Decode applies raw binary transcoding behaviour to decode into a Go type. func (t *RawBinaryTranscoder) Decode(bytes []byte, flags uint32, out interface{}) error { valueType, compression := gocbcore.DecodeCommonFlags(flags) // Make sure compression is disabled if compression != gocbcore.NoCompression { return errors.New("unexpected value compression") } // Normal types of decoding if valueType == gocbcore.BinaryType { switch typedOut := out.(type) { case *[]byte: *typedOut = bytes return nil case *interface{}: *typedOut = bytes return nil default: return errors.New("you must encode binary in a byte array or interface") } } else if valueType == gocbcore.StringType { return errors.New("only binary datatype is supported by RawBinaryTranscoder") } else if valueType == gocbcore.JSONType { return errors.New("only binary datatype is supported by RawBinaryTranscoder") } return errors.New("unexpected expectedFlags value") } // Encode applies raw binary transcoding behaviour to encode a Go type. func (t *RawBinaryTranscoder) Encode(value interface{}) ([]byte, uint32, error) { var bytes []byte var flags uint32 switch typeValue := value.(type) { case []byte: bytes = typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression) case *[]byte: bytes = *typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression) case *interface{}: return t.Encode(*typeValue) default: return nil, 0, makeInvalidArgumentsError("only raw binary data is supported by RawBinaryTranscoder") } // No compression supported currently return bytes, flags, nil } // LegacyTranscoder implements the behaviour for a backward-compatible transcoder. This transcoder implements // behaviour matching that of gocb v1. // // This will apply the following behavior to the value: // binary ([]byte) -> binary bytes, Binary expectedFlags. // string -> string bytes, String expectedFlags. // default -> JSON value, JSON expectedFlags. type LegacyTranscoder struct { } // NewLegacyTranscoder returns a new LegacyTranscoder. func NewLegacyTranscoder() *LegacyTranscoder { return &LegacyTranscoder{} } // Decode applies legacy transcoding behaviour to decode into a Go type. func (t *LegacyTranscoder) Decode(bytes []byte, flags uint32, out interface{}) error { valueType, compression := gocbcore.DecodeCommonFlags(flags) // Make sure compression is disabled if compression != gocbcore.NoCompression { return errors.New("unexpected value compression") } // Normal types of decoding if valueType == gocbcore.BinaryType { switch typedOut := out.(type) { case *[]byte: *typedOut = bytes return nil case *interface{}: *typedOut = bytes return nil default: return errors.New("you must encode binary in a byte array or interface") } } else if valueType == gocbcore.StringType { switch typedOut := out.(type) { case *string: *typedOut = string(bytes) return nil case *interface{}: *typedOut = string(bytes) return nil default: return errors.New("you must encode a string in a string or interface") } } else if valueType == gocbcore.JSONType { err := json.Unmarshal(bytes, &out) if err != nil { return err } return nil } return errors.New("unexpected expectedFlags value") } // Encode applies legacy transcoding behavior to encode a Go type. func (t *LegacyTranscoder) Encode(value interface{}) ([]byte, uint32, error) { var bytes []byte var flags uint32 var err error switch typeValue := value.(type) { case []byte: bytes = typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression) case *[]byte: bytes = *typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression) case string: bytes = []byte(typeValue) flags = gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression) case *string: bytes = []byte(*typeValue) flags = gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression) case json.RawMessage: bytes = typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *json.RawMessage: bytes = *typeValue flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) case *interface{}: return t.Encode(*typeValue) default: bytes, err = json.Marshal(value) if err != nil { return nil, 0, err } flags = gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression) } // No compression supported currently return bytes, flags, nil } gocb-2.6.3/transcoding_test.go000066400000000000000000000476521441755043100163570ustar00rootroot00000000000000package gocb import ( "encoding/json" "reflect" "testing" gocbcore "github.com/couchbase/gocbcore/v10" ) func (suite *UnitTestSuite) TestEncode() { byteArray := []byte("something") rawString := "something" rawMsg := json.RawMessage("hello") rawNumber := 22022 jsonStruct := struct { Name string `json:"name"` }{Name: "something"} jsonValue, err := json.Marshal(jsonStruct) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } stringValue, err := json.Marshal(rawString) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } numberValue, err := json.Marshal(rawNumber) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } var rawInterface interface{} = rawString type test struct { name string args interface{} expected []byte expectedFlags uint32 wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { name: "byte array", args: byteArray, wantErr: true, }, { name: "byte point array", args: &byteArray, wantErr: true, }, { name: "string", args: rawString, expected: stringValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "string pointer", args: &rawString, expected: stringValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json", args: jsonStruct, expected: jsonValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json pointer", args: &jsonStruct, expected: jsonValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json.RawMessage", args: rawMsg, expected: rawMsg, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json.RawMessage pointer", args: &rawMsg, expected: rawMsg, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "number", args: rawNumber, expected: numberValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "number pointer", args: &rawNumber, expected: numberValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "interface", args: rawInterface, expected: stringValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "interface pointer", args: &rawInterface, expected: stringValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, }, NewLegacyTranscoder(): { { name: "byte array", args: byteArray, expected: byteArray, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression), wantErr: false, }, { name: "byte point array", args: &byteArray, expected: byteArray, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression), wantErr: false, }, { name: "string", args: rawString, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, { name: "string pointer", args: &rawString, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, { name: "json", args: jsonStruct, expected: jsonValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json pointer", args: &jsonStruct, expected: jsonValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json.RawMessage", args: rawMsg, expected: rawMsg, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json.RawMessage pointer", args: &rawMsg, expected: rawMsg, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "number", args: rawNumber, expected: numberValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "number pointer", args: &rawNumber, expected: numberValue, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "interface", args: rawInterface, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, { name: "interface pointer", args: &rawInterface, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, }, NewRawJSONTranscoder(): { { name: "byte array", args: byteArray, expected: byteArray, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "byte point array", args: &byteArray, expected: byteArray, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "string", args: rawString, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "string pointer", args: &rawString, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json", args: jsonStruct, wantErr: true, }, { name: "json pointer", args: &jsonStruct, wantErr: true, }, { name: "json.RawMessage", args: rawMsg, expected: rawMsg, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "json.RawMessage pointer", args: &rawMsg, expected: rawMsg, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "number", args: rawNumber, wantErr: true, }, { name: "number pointer", args: &rawNumber, wantErr: true, }, { name: "interface", args: rawInterface, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, { name: "interface pointer", args: &rawInterface, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.JSONType, gocbcore.NoCompression), wantErr: false, }, }, NewRawStringTranscoder(): { { name: "byte array", args: byteArray, wantErr: true, }, { name: "byte point array", args: &byteArray, wantErr: true, }, { name: "string", args: rawString, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, { name: "string pointer", args: &rawString, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, { name: "json", args: jsonStruct, wantErr: true, }, { name: "json pointer", args: &jsonStruct, wantErr: true, }, { name: "json.RawMessage", args: rawMsg, wantErr: true, }, { name: "json.RawMessage pointer", args: &rawMsg, wantErr: true, }, { name: "number", args: rawNumber, wantErr: true, }, { name: "number pointer", args: &rawNumber, wantErr: true, }, { name: "interface", args: rawInterface, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, { name: "interface pointer", args: &rawInterface, expected: []byte(rawString), expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.StringType, gocbcore.NoCompression), wantErr: false, }, }, NewRawBinaryTranscoder(): { { name: "byte array", args: byteArray, expected: byteArray, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression), wantErr: false, }, { name: "byte point array", args: &byteArray, expected: byteArray, expectedFlags: gocbcore.EncodeCommonFlags(gocbcore.BinaryType, gocbcore.NoCompression), wantErr: false, }, { name: "string", args: rawString, wantErr: true, }, { name: "string pointer", wantErr: true, }, { name: "json", args: jsonStruct, wantErr: true, }, { name: "json pointer", args: &jsonStruct, wantErr: true, }, { name: "json.RawMessage", args: rawMsg, wantErr: true, }, { name: "json.RawMessage pointer", args: &rawMsg, wantErr: true, }, { name: "number", args: rawNumber, wantErr: true, }, { name: "number pointer", args: &rawNumber, wantErr: true, }, }, } for transcoder, transcoderTests := range tests { for _, tt := range transcoderTests { suite.T().Run(tt.name, func(t *testing.T) { actual, flags, err := transcoder.Encode(tt.args) name := reflect.ValueOf(transcoder).Type() if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } if flags != tt.expectedFlags { t.Errorf("%s got1 = %v, want %v", name, flags, tt.expectedFlags) } }) } } } func (suite *UnitTestSuite) TestDecodeJSON() { type jsonType struct { Name string `json:"name"` } jsonStruct := jsonType{Name: "something"} jsonValue, err := json.Marshal(jsonStruct) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } type test struct { bytes []byte flags uint32 expected jsonType wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: jsonStruct, wantErr: false, }, }, NewLegacyTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: jsonStruct, wantErr: false, }, }, NewRawJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawStringTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawBinaryTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, } for transcoder, transcoderTests := range tests { name := reflect.ValueOf(transcoder).Type().String() for _, tt := range transcoderTests { suite.T().Run(name, func(t *testing.T) { var actual jsonType err := transcoder.Decode(tt.bytes, tt.flags, &actual) if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } }) } } } func (suite *UnitTestSuite) TestDecodeJSONInterface() { type jsonType struct { Name string `json:"name"` } jsonStruct := jsonType{Name: "something"} jsonValue, err := json.Marshal(jsonStruct) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } type test struct { bytes []byte flags uint32 expected jsonType wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: jsonStruct, wantErr: false, }, }, NewLegacyTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: jsonStruct, wantErr: false, }, }, NewRawJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawStringTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawBinaryTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, } for transcoder, transcoderTests := range tests { name := reflect.ValueOf(transcoder).Type().String() for _, tt := range transcoderTests { suite.T().Run(name, func(t *testing.T) { str := jsonType{} var actual interface{} = &str err := transcoder.Decode(tt.bytes, tt.flags, &actual) if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, &tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } }) } } } func (suite *UnitTestSuite) TestDecodeJSONString() { rawString := "something" jsonValue, err := json.Marshal(rawString) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } type test struct { bytes []byte flags uint32 expected string wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: rawString, wantErr: false, }, }, NewLegacyTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: rawString, wantErr: false, }, }, NewRawJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: string(jsonValue), wantErr: false, }, }, NewRawStringTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawBinaryTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, } for transcoder, transcoderTests := range tests { name := reflect.ValueOf(transcoder).Type().String() for _, tt := range transcoderTests { suite.T().Run(name, func(t *testing.T) { var actual string err := transcoder.Decode(tt.bytes, tt.flags, &actual) if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } }) } } } func (suite *UnitTestSuite) TestDecodeJSONNumber() { rawNumber := 22022 jsonValue, err := json.Marshal(rawNumber) if err != nil { suite.T().Fatalf("failed to marshal json: %v", err) } type test struct { bytes []byte flags uint32 expected int wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: rawNumber, wantErr: false, }, }, NewLegacyTranscoder(): { { bytes: jsonValue, flags: 0x2000000, expected: rawNumber, wantErr: false, }, }, NewRawJSONTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawStringTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, NewRawBinaryTranscoder(): { { bytes: jsonValue, flags: 0x2000000, wantErr: true, }, }, } for transcoder, transcoderTests := range tests { name := reflect.ValueOf(transcoder).Type().String() for _, tt := range transcoderTests { suite.T().Run(name, func(t *testing.T) { var actual int err := transcoder.Decode(tt.bytes, tt.flags, &actual) if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } }) } } } func (suite *UnitTestSuite) TestDecodeBinary() { binary := []byte("222222") type test struct { bytes []byte flags uint32 expected []byte wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { bytes: binary, flags: 3 << 24, wantErr: true, }, }, NewLegacyTranscoder(): { { bytes: binary, flags: 3 << 24, expected: binary, wantErr: false, }, }, NewRawJSONTranscoder(): { { bytes: binary, flags: 3 << 24, wantErr: true, }, }, NewRawStringTranscoder(): { { bytes: binary, flags: 3 << 24, wantErr: true, }, }, NewRawBinaryTranscoder(): { { bytes: binary, flags: 3 << 24, expected: binary, wantErr: false, }, }, } for transcoder, transcoderTests := range tests { name := reflect.ValueOf(transcoder).Type().String() for _, tt := range transcoderTests { suite.T().Run(name, func(t *testing.T) { var actual []byte err := transcoder.Decode(tt.bytes, tt.flags, &actual) if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } }) } } } func (suite *UnitTestSuite) TestDecodeString() { rawString := "something" type test struct { bytes []byte flags uint32 expected string wantErr bool } tests := map[Transcoder][]test{ NewJSONTranscoder(): { { bytes: []byte(rawString), flags: 4 << 24, wantErr: true, }, }, NewLegacyTranscoder(): { { bytes: []byte(rawString), flags: 4 << 24, expected: rawString, wantErr: false, }, }, NewRawJSONTranscoder(): { { bytes: []byte(rawString), flags: 4 << 24, wantErr: true, }, }, NewRawStringTranscoder(): { { bytes: []byte(rawString), flags: 4 << 24, expected: rawString, wantErr: false, }, }, NewRawBinaryTranscoder(): { { bytes: []byte(rawString), flags: 4 << 24, wantErr: true, }, }, } for transcoder, transcoderTests := range tests { name := reflect.ValueOf(transcoder).Type().String() for _, tt := range transcoderTests { suite.T().Run(name, func(t *testing.T) { var actual string err := transcoder.Decode(tt.bytes, tt.flags, &actual) if (err != nil) != tt.wantErr { t.Errorf("%s error = %v, wantErr %v", name, err, tt.wantErr) return } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("%s got = %v, want %v", name, actual, tt.expected) } }) } } } gocb-2.6.3/utils_test.go000066400000000000000000000044641441755043100151760ustar00rootroot00000000000000package gocb import ( "encoding/json" "github.com/google/uuid" "io" "io/ioutil" "testing" ) type testBeerDocument struct { ABV float32 `json:"abv,omitempty"` BreweryID string `json:"brewery_id,omitempty"` Category string `json:"category,omitempty"` Description string `json:"description,omitempty"` IBU int `json:"IBU,omitempty"` Name string `json:"name,omitempty"` SRM int `json:"srm,omitempty"` Style string `json:"style,omitempty"` Type string `json:"type,omitempty"` UPC int `json:"upc,omitempty"` Updated string `json:"updated,omitempty"` } type testBreweryGeo struct { Accuracy string `json:"accuracy,omitempty"` Lat float32 `json:"lat,omitempty"` Lon float32 `json:"lon,omitempty"` } type testBreweryDocument struct { City string `json:"city,omitempty"` Code string `json:"code,omitempty"` Country string `json:"country,omitempty"` Description string `json:"description,omitempty"` Geo testBreweryGeo `json:"geo,omitempty"` Name string `json:"name,omitempty"` Phone string `json:"phone,omitempty"` State string `json:"state,omitempty"` Type string `json:"type,omitempty"` Updated string `json:"updated,omitempty"` Website string `json:"website,omitempty"` Service string `json:"service,omitempty"` } type testMetadata struct { } func loadRawTestDataset(dataset string) ([]byte, error) { return ioutil.ReadFile("testdata/" + dataset + ".json") } func loadJSONTestDataset(dataset string, valuePtr interface{}) error { bytes, err := loadRawTestDataset(dataset) if err != nil { return err } err = json.Unmarshal(bytes, &valuePtr) if err != nil { return err } return nil } func marshal(t *testing.T, value interface{}) []byte { b, err := json.Marshal(value) if err != nil { t.Fatalf("Could not marshal value: %v", err) } return b } // If using with an empty prefix ensure you prepend at least a single letter so the doc ID does not begin with a number func generateDocId(prefix string) string { return prefix + uuid.NewString()[:6] } type testReadCloser struct { io.Reader closeErr error } func (trc *testReadCloser) Close() error { return trc.closeErr } gocb-2.6.3/version.go000066400000000000000000000004221441755043100144520ustar00rootroot00000000000000package gocb // Version returns a string representation of the current SDK version. func Version() string { return goCbVersionStr } // Identifier returns a string representation of the current SDK identifier. func Identifier() string { return "gocb/" + goCbVersionStr } gocb-2.6.3/viewquery_options.go000066400000000000000000000127531441755043100166120ustar00rootroot00000000000000package gocb import ( "bytes" "context" "encoding/json" "net/url" "strconv" "time" ) // ViewScanConsistency specifies the consistency required for a view query. type ViewScanConsistency uint const ( // ViewScanConsistencyNotBounded indicates that no special behaviour should be used. ViewScanConsistencyNotBounded ViewScanConsistency = iota + 1 // ViewScanConsistencyRequestPlus indicates to update the index before querying it. ViewScanConsistencyRequestPlus // ViewScanConsistencyUpdateAfter indicates to update the index asynchronously after querying. ViewScanConsistencyUpdateAfter ) // ViewOrdering specifies the ordering for the view queries results. type ViewOrdering uint const ( // ViewOrderingAscending indicates the query results should be sorted from lowest to highest. ViewOrderingAscending ViewOrdering = iota + 1 // ViewOrderingDescending indicates the query results should be sorted from highest to lowest. ViewOrderingDescending ) // ViewErrorMode pecifies the behaviour of the query engine should an error occur during the gathering of // view index results which would result in only partial results being available. type ViewErrorMode uint const ( // ViewErrorModeContinue indicates to continue gathering results on error. ViewErrorModeContinue ViewErrorMode = iota + 1 // ViewErrorModeStop indicates to stop gathering results on error ViewErrorModeStop ) // ViewOptions represents the options available when executing view query. type ViewOptions struct { ScanConsistency ViewScanConsistency Skip uint32 Limit uint32 Order ViewOrdering Reduce bool Group bool GroupLevel uint32 Key interface{} Keys []interface{} StartKey interface{} EndKey interface{} InclusiveEnd bool StartKeyDocID string EndKeyDocID string OnError ViewErrorMode Debug bool ParentSpan RequestSpan // Raw provides a way to provide extra parameters in the request body for the query. Raw map[string]string Namespace DesignDocumentNamespace Timeout time.Duration RetryStrategy RetryStrategy // Using a deadlined Context alongside a Timeout will cause the shorter of the two to cause cancellation, this // also applies to global level timeouts. // UNCOMMITTED: This API may change in the future. Context context.Context // Internal: This should never be used and is not supported. Internal struct { User string } } func (opts *ViewOptions) toURLValues() (*url.Values, error) { options := &url.Values{} if opts.ScanConsistency != 0 { if opts.ScanConsistency == ViewScanConsistencyRequestPlus { options.Set("stale", "false") } else if opts.ScanConsistency == ViewScanConsistencyNotBounded { options.Set("stale", "ok") } else if opts.ScanConsistency == ViewScanConsistencyUpdateAfter { options.Set("stale", "update_after") } else { return nil, makeInvalidArgumentsError("unexpected stale option") } } if opts.Skip != 0 { options.Set("skip", strconv.FormatUint(uint64(opts.Skip), 10)) } if opts.Limit != 0 { options.Set("limit", strconv.FormatUint(uint64(opts.Limit), 10)) } if opts.Order != 0 { if opts.Order == ViewOrderingAscending { options.Set("descending", "false") } else if opts.Order == ViewOrderingDescending { options.Set("descending", "true") } else { return nil, makeInvalidArgumentsError("unexpected order option") } } options.Set("reduce", "false") // is this line necessary? if opts.Reduce { options.Set("reduce", "true") // Only set group if a reduce view if opts.Group { options.Set("group", "true") } if opts.GroupLevel != 0 { options.Set("group_level", strconv.FormatUint(uint64(opts.GroupLevel), 10)) } } if opts.Key != nil { jsonKey, err := opts.marshalJSON(opts.Key) if err != nil { return nil, err } options.Set("key", string(jsonKey)) } if len(opts.Keys) > 0 { jsonKeys, err := opts.marshalJSON(opts.Keys) if err != nil { return nil, err } options.Set("keys", string(jsonKeys)) } if opts.StartKey != nil { jsonStartKey, err := opts.marshalJSON(opts.StartKey) if err != nil { return nil, err } options.Set("startkey", string(jsonStartKey)) } else { options.Del("startkey") } if opts.EndKey != nil { jsonEndKey, err := opts.marshalJSON(opts.EndKey) if err != nil { return nil, err } options.Set("endkey", string(jsonEndKey)) } else { options.Del("endkey") } if opts.StartKey != nil || opts.EndKey != nil { if opts.InclusiveEnd { options.Set("inclusive_end", "true") } else { options.Set("inclusive_end", "false") } } if opts.StartKeyDocID == "" { options.Del("startkey_docid") } else { options.Set("startkey_docid", opts.StartKeyDocID) } if opts.EndKeyDocID == "" { options.Del("endkey_docid") } else { options.Set("endkey_docid", opts.EndKeyDocID) } if opts.OnError > 0 { if opts.OnError == ViewErrorModeContinue { options.Set("on_error", "continue") } else if opts.OnError == ViewErrorModeStop { options.Set("on_error", "stop") } else { return nil, makeInvalidArgumentsError("unexpected onerror option") } } if opts.Debug { options.Set("debug", "true") } if opts.Raw != nil { for k, v := range opts.Raw { options.Set(k, v) } } return options, nil } func (opts *ViewOptions) marshalJSON(value interface{}) ([]byte, error) { buf := new(bytes.Buffer) enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) err := enc.Encode(value) if err != nil { return nil, err } return buf.Bytes(), nil } gocb-2.6.3/viewquery_options_test.go000066400000000000000000000115111441755043100176400ustar00rootroot00000000000000package gocb import ( "fmt" "math/rand" "net/url" ) func (suite *UnitTestSuite) TestViewQueryOptionsToURLValues() { for i := 0; i < 50; i++ { opts := suite.testCreateViewQueryOptions(int64(i)) optValues, err := opts.toURLValues() if opts.ScanConsistency > ViewScanConsistencyUpdateAfter || opts.ScanConsistency < 0 { if err == nil { suite.T().Fatalf("Expected an error for invalid stale value") } else { continue } } if opts.Order > ViewOrderingDescending || opts.Order < 0 { if err == nil { suite.T().Fatalf("Expected an error for invalid order value") } else { continue } } if err != nil { suite.T().Fatalf("Expected no error but was %v", err) } if opts.ScanConsistency == 0 { suite.testAssertViewOption("", "stale", optValues) } else if opts.ScanConsistency == ViewScanConsistencyRequestPlus { suite.testAssertViewOption("false", "stale", optValues) } else if opts.ScanConsistency == ViewScanConsistencyNotBounded { suite.testAssertViewOption("ok", "stale", optValues) } else if opts.ScanConsistency == ViewScanConsistencyUpdateAfter { suite.testAssertViewOption("update_after", "stale", optValues) } if opts.Skip == 0 { suite.testAssertViewOption("", "skip", optValues) } else { suite.testAssertViewOption(fmt.Sprintf("%d", opts.Skip), "skip", optValues) } if opts.Limit == 0 { suite.testAssertViewOption("", "limit", optValues) } else { suite.testAssertViewOption(fmt.Sprintf("%d", opts.Limit), "limit", optValues) } if opts.Order == 0 { suite.testAssertViewOption("", "descending", optValues) } else if opts.Order == ViewOrderingAscending { suite.testAssertViewOption("false", "descending", optValues) } else if opts.Order == ViewOrderingDescending { suite.testAssertViewOption("true", "descending", optValues) } if opts.Reduce { suite.testAssertViewOption("true", "reduce", optValues) if opts.Group { suite.testAssertViewOption("true", "group", optValues) } else { suite.testAssertViewOption("", "group", optValues) } if opts.GroupLevel == 0 { suite.testAssertViewOption("", "group_level", optValues) } else { suite.testAssertViewOption(fmt.Sprintf("%d", opts.GroupLevel), "group_level", optValues) } } else { suite.testAssertViewOption("false", "reduce", optValues) suite.testAssertViewOption("", "group", optValues) suite.testAssertViewOption("", "group_level", optValues) } if opts.Key == nil { suite.testAssertViewOption("", "key", optValues) } else { suite.testAssertViewOption("[\"key1\"]\n", "key", optValues) } if len(opts.Keys) == 0 { suite.testAssertViewOption("", "keys", optValues) } else { suite.testAssertViewOption("[\"key2\",\"key3\",{\"key\":\"key4\"}]\n", "keys", optValues) } } } func (suite *UnitTestSuite) testAssertViewOption(expected string, key string, optValues *url.Values) { val := optValues.Get(key) if val != expected { suite.T().Fatalf("Values had incorrect %s, expected %s but was %s", key, expected, val) } } func (suite *UnitTestSuite) testCreateViewQueryOptions(seed int64) *ViewOptions { opts := &ViewOptions{} rand.Seed(seed) randVal := rand.Intn(6) if randVal == 1 { opts.ScanConsistency = ViewScanConsistencyRequestPlus } else if randVal == 2 { opts.ScanConsistency = ViewScanConsistencyNotBounded } else if randVal == 3 { opts.ScanConsistency = ViewScanConsistencyUpdateAfter } else if randVal == 4 { opts.ScanConsistency = 5 } randVal = rand.Intn(2) if randVal == 1 { opts.Skip = uint32(rand.Intn(10)) } randVal = rand.Intn(2) if randVal == 1 { opts.Limit = uint32(rand.Intn(10)) } randVal = rand.Intn(4) if randVal == 1 { opts.Order = ViewOrderingAscending } else if randVal == 2 { opts.Order = ViewOrderingDescending } else if randVal == 3 { opts.Order = 3 } randVal = rand.Intn(2) if randVal == 1 { opts.Reduce = true } randVal = rand.Intn(2) if randVal == 1 { opts.Group = true } randVal = rand.Intn(2) if randVal == 1 { opts.GroupLevel = uint32(rand.Intn(5)) } randVal = rand.Intn(2) if randVal == 1 { opts.Key = []string{"key1"} } randVal = rand.Intn(2) if randVal == 1 { opts.Keys = []interface{}{"key2", "key3", struct { Key string `json:"key"` }{"key4"}} } randVal = rand.Intn(2) if randVal == 1 { randVal = rand.Intn(2) if randVal == 1 { opts.StartKey = "keystart" } randVal = rand.Intn(2) if randVal == 1 { opts.EndKey = "keyend" } randVal = rand.Intn(3) if randVal == 1 { opts.InclusiveEnd = true } else if randVal == 2 { opts.InclusiveEnd = false } } randVal = rand.Intn(2) if randVal == 1 { opts.StartKeyDocID = "rangeStart" } randVal = rand.Intn(2) if randVal == 1 { opts.EndKeyDocID = "rangeEnd" } randVal = rand.Intn(2) if randVal == 1 { opts.Raw = map[string]string{"key1": "param1", "$key2": "param2"} } return opts }