Repository: abronan/valkeyrie Branch: main Commit: a5556e0b4245 Files: 23 Total size: 60.5 KB Directory structure: gitextract__0jbl6wy/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── new_store.yml │ ├── dependabot.yml │ └── workflows/ │ └── build.yml ├── .gitignore ├── .golangci.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── docs/ │ └── examples.md ├── go.mod ├── go.sum ├── maintainers.md ├── mock_test.go ├── readme.md ├── store/ │ ├── errors.go │ ├── helpers.go │ └── store.go ├── testsuite/ │ └── suite.go ├── valkeyrie.go └── valkeyrie_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐞 Bug Report description: Create a report to help us improve. body: - type: checkboxes id: terms attributes: label: Welcome options: - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - label: Yes, I've included all information below (version, config, etc). required: true - type: textarea id: problem attributes: label: Description of the problem placeholder: Your problem description validations: required: true - type: input id: version attributes: label: Version of Valkeyrie validations: required: true - type: textarea id: go-env attributes: label: Go environment value: |-
```console $ go version && go env # paste output here ```
validations: required: true - type: textarea id: code-example attributes: label: Code example or link to a public repository value: |-
```go // add your code here ```
validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: ❓ Questions url: https://github.com/kvtools/valkeyrie/discussions about: If you have a question, or are looking for advice, please post on our Discussions forum! ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: 💡 Feature request description: "Suggest an idea for this project." body: - type: textarea id: problem attributes: label: Your feature request related to a problem? Please describe. placeholder: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]" validations: required: true - type: textarea id: solution attributes: label: Describe the solution you'd like. placeholder: "A clear and concise description of what you want to happen." validations: required: true - type: textarea id: alternatives attributes: label: Describe alternatives you've considered. placeholder: "A clear and concise description of any alternative solutions or features you've considered." validations: required: true - type: textarea id: additional attributes: label: Additional context. placeholder: "Add any other context or screenshots about the feature request here." validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/new_store.yml ================================================ name: 🧩 Add a new type of store description: "Proposal" body: - type: input id: store-name attributes: label: The name of the store. validations: required: true - type: input id: repo-url attributes: label: The URL of your repository. validations: required: true - type: textarea id: description attributes: label: Provide information about the store. placeholder: "A clear and concise description." validations: required: false ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: monthly cooldown: default-days: 7 groups: go: patterns: - "*" # Group all updates into a single larger pull request. - package-ecosystem: github-actions directory: "/" schedule: interval: monthly cooldown: default-days: 7 groups: github-actions: patterns: - "*" # Group all updates into a single larger pull request. ================================================ FILE: .github/workflows/build.yml ================================================ name: Build and test on: [push, pull_request] env: GOLANGCI_LINT_VERSION: v2.11 permissions: {} jobs: build: runs-on: ubuntu-latest strategy: matrix: go-version: [stable, oldstable] steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Download and check dependencies run: | go mod tidy --diff - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} uses: golangci/golangci-lint-action@v9 with: version: ${{ env.GOLANGCI_LINT_VERSION }} - name: Run tests run: make test ================================================ FILE: .gitignore ================================================ .idea/ .DS_Store *.iml dump.rdb ================================================ FILE: .golangci.yml ================================================ version: "2" formatters: enable: - gci - gofumpt settings: gofumpt: extra-rules: false linters: default: none enable: - asasalint - asciicheck - bidichk - bodyclose - contextcheck - depguard - dogsled - durationcheck - err113 - errcheck - errname - errorlint - forbidigo - forcetypeassert - funlen - gochecknoglobals - gochecknoinits - gocognit - goconst - gocritic - gocyclo - godot - godox - goheader - gomoddirectives - gomodguard - goprintffuncname - gosec - govet - importas - ineffassign - makezero - misspell - nakedret - nestif - nilerr - noctx - nolintlint - predeclared - promlinter - revive - staticcheck - tagliatelle - thelper - unconvert - unparam - unused - usestdlibvars - wastedassign settings: depguard: rules: main: deny: - pkg: github.com/instana/testify desc: not allowed - pkg: github.com/sirupsen/logrus desc: not allowed - pkg: github.com/pkg/errors desc: Should be replaced by standard lib errors package funlen: lines: -1 statements: 45 goconst: min-len: 5 min-occurrences: 3 gocritic: disabled-checks: - unnamedResult - sloppyReassign - rangeValCopy - octalLiteral - paramTypeCombine # already handle by gofumpt.extra-rules enabled-tags: - diagnostic - style - performance settings: hugeParam: sizeThreshold: 100 gocyclo: min-complexity: 15 godox: keywords: - FIXME govet: disable: - fieldalignment enable-all: true misspell: locale: US staticcheck: checks: - all exclusions: warn-unused: true rules: - linters: - funlen - goconst path: (.+)_test.go - linters: - gochecknoglobals path: valkeyrie.go issues: max-issues-per-linter: 0 max-same-issues: 0 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing All contributions are useful, whether it is a simple typo, a more complex change, or just pointing out an issue. We welcome any contribution so feel free to take part in the discussions. If you want to contribute to the project, please make sure to review this document carefully. - [Setting up the environment](#working-environment) - [Before submitting a change](#before-submitting-a-change) - [Your first pull request](#your-first-pull-request) ## Working Environment ### Prerequisites - Git - Golang - One or all of the supported datastore (Zookeeper / Consul / Etcd / Redis / BoltDB) ### Installing Golang Install golang using your favorite package manager on Linux or with the archive following these [Guidelines](https://golang.org/doc/install). An easy way to get started on macOS is to use [homebrew](https://brew.sh) and type `brew install go` in a shell. ### Local testing of key/value stores In addition to installing golang, you will need to install some or all of the key value stores for testing. Refer to each of these stores documentation in order to proceed with installation. Generally, the tests are using the **default configuration** with the **default port** to connect to a store and run the test suite. To test a change, you can run the test suite with the following command: ```bash make test ``` ## Before submitting a change Make sure you check each of these items before you submit a pull request to avoid many unnecessary back and forth in GitHub comments (and will help us review and include the change as soon as possible): - **Open an issue** to clearly state the problem. This will be helpful to keep track of what needs to be fixed. This also helps sort and prioritize issues. - **Run the following command**: `make validate`, to ensure that your code is properly formatted. - **For non-trivial changes, write a test**: this is to ensure that we don't encounter any regression in the future. - **Write a complete description** for your pull request (avoid using `-m` flag when committing a change unless it is a trivial one). - **Sign-off your commits** using the `-s` flag (you can configure an alias to `git commit` adding `-s` for convenience). - **Squash your commits** if the pull requests includes many commits that are related. This is to maintain a clean history of the change and better identify faulty commits if reverting a change is ever needed. We will tell you if squashing your commits is necessary. - **If the change is solving one or more issues listed on the repository**: you can reference the issue in your comment with `closes #XXX` or `fixes #XXX`. This will automatically close the related issues on merging the change. See [GitHub documentation](https://help.github.com/articles/closing-issues-using-keywords/) for more details. Finally, submit your *Pull Request*. ## Your first Pull Request You made it to your first Pull Request? It's only the start of the process. Following steps may include a discussion on the design and tradeoffs of your proposed solution. Additionally, there will be a *code review process* to find out potential bugs. Part of being a helpful community is to make sure we point out improvements and deliver actionable items to work towards fixing potential issues. Feel free to ask questions if you are stuck, so we can help you. *Don't be discouraged* if your change happens not to be included. All contributions are helpful in a way. Your PR most certainly made the discussion go forward in many aspects and helped to work towards our common goal of making the project better for everyone. **Welcome!** ================================================ FILE: LICENSE ================================================ 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 Copyright 2014-2016 Docker, Inc. Copyright 2021-2022 Valkeyrie authors 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. ================================================ FILE: Makefile ================================================ .PHONY: all all: validate test ## Run validates .PHONY: validate validate: golangci-lint run ## Run tests .PHONY: test test: go test -v -race ./... ================================================ FILE: docs/examples.md ================================================ # Examples This document contains useful example of usage for `valkeyrie`. It might not be complete but provides with general information on how to use the client. ## Create a Store and Use `Put`/`Get`/`Delete` ```go package main import ( "context" "log" "time" "github.com/kvtools/consul" "github.com/kvtools/valkeyrie" ) func main() { ctx := context.Background() config := &consul.Config{ ConnectionTimeout: 10 * time.Second, } kv, err := valkeyrie.NewStore(ctx, consul.StoreName, []string{"localhost:8500"}, config) if err != nil { log.Fatal("Cannot create store consul") } key := "foo" err = kv.Put(ctx, key, []byte("bar"), nil) if err != nil { log.Fatalf("Error trying to put value at key: %v", key) } pair, err := kv.Get(ctx, key, nil) if err != nil { log.Fatalf("Error trying accessing value at key: %v", key) } err = kv.Delete(ctx, key) if err != nil { log.Fatalf("Error trying to delete key %v", key) } log.Printf("value: %s", string(pair.Value)) } ``` ## List Keys ```go // List will list all the keys under `key` if it contains a set of child keys/values entries, err := kv.List(ctx, key, nil) for _, pair := range entries { fmt.Printf("key=%v - value=%v", pair.Key, string(pair.Value)) } ``` ## Watching for Events on a Single Key (`Watch`) You can use watches to watch modifications on a key. First you need to check if the key exists. If this is not the case, we need to create it using the `Put` function. ```go package example import ( "context" "fmt" "github.com/kvtools/valkeyrie/store" ) func watch(ctx context.Context, kv store.Store, key string) error { // Checking on the key before watching exists, _ := kv.Exists(ctx, key, nil) if !exists { err := kv.Put(ctx, key, []byte("bar"), nil) if err != nil { return fmt.Errorf("something went wrong when initializing key %v", key) } } events, err := kv.Watch(ctx, key, nil) if err != nil { return err } for { select { case pair := <-events: // Do something with events fmt.Printf("value changed on key %v: new value=%v", key, pair.Value) } } // ... return nil } ``` ## Watching for Events Happening on Child Keys (`WatchTree`) You can use watches to watch modifications on a key. First you need to check if the key exists. If this is not the case, we need to create it using the `Put` function. There is a special step here if you are using etcd **APIv2** and if you want your code to work across backends. `etcd` with **APIv2** makes the distinction between directories and keys, we need to make sure that the created key is considered as a directory by enforcing `IsDir` at `true`. ```go package example import ( "context" "fmt" "github.com/kvtools/valkeyrie/store" ) func watchTree(ctx context.Context, kv store.Store, key string) error { // Checking on the key before watching exists, _ := kv.Exists(ctx, key, nil) if !exists { // Do not forget `IsDir:true` if you are using etcd APIv2 err := kv.Put(ctx, key, []byte("bar"), &store.WriteOptions{IsDir: true}) if err != nil { return fmt.Errorf("something went wrong when initializing key %v", key) } } events, err := kv.WatchTree(ctx, key, nil) if err != nil { return err } select { case pairs := <-events: // Do something with events for _, pair := range pairs { fmt.Printf("value changed on key %v: new value=%v", key, pair.Value) } } // ... return nil } ``` ## Distributed Locking, Using Lock/Unlock ```go package example import ( "context" "fmt" "time" "github.com/kvtools/valkeyrie/store" ) func foo(ctx context.Context, kv store.Store) error { key := "lockKey" value := []byte("bar") // Initialize a distributed lock. TTL is optional, it is here to make sure that // the lock is released after the program that is holding the lock ends or crashes lock, err := kv.NewLock(key, &store.LockOptions{Value: value, TTL: 2 * time.Second}) if err != nil { return fmt.Errorf("something went wrong when trying to initialize the Lock") } // Try to lock the key, the call to Lock() is blocking _, err = lock.Lock(nil) if err != nil { return fmt.Errorf("something went wrong when trying to lock key %v", key) } // Get should work because we are holding the key pair, err := kv.Get(ctx, key, nil) if err != nil { return fmt.Errorf("key %v has value %v", key, pair.Value) } // Unlock the key err = lock.Unlock(ctx) if err != nil { return fmt.Errorf("something went wrong when trying to unlock key %v", key) } // ... return nil } ``` ================================================ FILE: go.mod ================================================ module github.com/kvtools/valkeyrie go 1.22 require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: maintainers.md ================================================ - [Alexandre Beslic](https://github.com/abronan) - [Victor Castell](https://github.com/victorcoder) - [Nicolas Mengin](https://github.com/nmengin) - [Maciej Winnicki](https://github.com/mthenw) - [Ludovic Fernandez](https://github.com/ldez) - [Kevin Pollet](https://github.com/kevinpollet) - [Tom Moulard](https://github.com/tomMoulard) ================================================ FILE: mock_test.go ================================================ package valkeyrie import ( "context" "github.com/kvtools/valkeyrie/store" ) const testStoreName = "mock" func newStore(ctx context.Context, endpoints []string, options Config) (store.Store, error) { cfg, ok := options.(*Config) if !ok && options != nil { return nil, &store.InvalidConfigurationError{Store: testStoreName, Config: options} } return New(ctx, endpoints, cfg) } type Mock struct { cfg *Config } // New creates a new Mock client. // //nolint:gocritic func New(_ context.Context, _ []string, cfg *Config) (*Mock, error) { return &Mock{cfg: cfg}, nil } func (m Mock) Put(_ context.Context, _ string, _ []byte, _ *store.WriteOptions) error { panic("implement me") } func (m Mock) Get(_ context.Context, _ string, _ *store.ReadOptions) (*store.KVPair, error) { panic("implement me") } func (m Mock) Delete(_ context.Context, _ string) error { panic("implement me") } func (m Mock) Exists(_ context.Context, _ string, _ *store.ReadOptions) (bool, error) { panic("implement me") } func (m Mock) Watch(_ context.Context, _ string, _ *store.ReadOptions) (<-chan *store.KVPair, error) { panic("implement me") } func (m Mock) WatchTree(_ context.Context, _ string, _ *store.ReadOptions) (<-chan []*store.KVPair, error) { panic("implement me") } func (m Mock) NewLock(_ context.Context, _ string, _ *store.LockOptions) (store.Locker, error) { panic("implement me") } func (m Mock) List(_ context.Context, _ string, _ *store.ReadOptions) ([]*store.KVPair, error) { panic("implement me") } func (m Mock) DeleteTree(_ context.Context, _ string) error { panic("implement me") } func (m Mock) AtomicPut(_ context.Context, _ string, _ []byte, _ *store.KVPair, _ *store.WriteOptions) (bool, *store.KVPair, error) { panic("implement me") } func (m Mock) AtomicDelete(_ context.Context, _ string, _ *store.KVPair) (bool, error) { panic("implement me") } func (m Mock) Close() error { panic("implement me") } ================================================ FILE: readme.md ================================================

golangci-lint logo

Valkeyrie

Distributed Key/Value Store Abstraction Library

# Valkeyrie [![Go Reference](https://pkg.go.dev/badge/github.com/kvtools/valkeyrie.svg)](https://pkg.go.dev/github.com/kvtools/valkeyrie) [![Build and test](https://github.com/kvtools/valkeyrie/actions/workflows/build.yml/badge.svg)](https://github.com/kvtools/valkeyrie/actions/workflows/build.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/kvtools/valkeyrie)](https://goreportcard.com/report/github.com/kvtools/valkeyrie) `valkeyrie` provides a Go native library to store metadata using Distributed Key/Value stores (or common databases). Its goal is to abstract common store operations (`Get`, `Put`, `List`, etc.) for multiple Key/Value store backends. For example, you can easily implement a generic *Leader Election* algorithm on top of it (see the [docker/leadership](https://github.com/docker/leadership) repository). The benefit of `valkeyrie` is not to duplicate the code for programs that should support multiple distributed Key/Value stores such as `Consul`/`etcd`/`zookeeper`, etc. ## Examples of Usage You can refer to [Examples](https://github.com/kvtools/valkeyrie/blob/master/docs/examples.md) for a basic overview of the library. ```go package main import ( "context" "log" "time" "github.com/kvtools/consul" "github.com/kvtools/valkeyrie" ) func main() { ctx := context.Background() config := &consul.Config{ ConnectionTimeout: 10 * time.Second, } kv, err := valkeyrie.NewStore(ctx, consul.StoreName, []string{"localhost:8500"}, config) if err != nil { log.Fatal("Cannot create store consul") } key := "foo" err = kv.Put(ctx, key, []byte("bar"), nil) if err != nil { log.Fatalf("Error trying to put value at key: %v", key) } pair, err := kv.Get(ctx, key, nil) if err != nil { log.Fatalf("Error trying accessing value at key: %v", key) } log.Printf("value: %s", string(pair.Value)) err = kv.Delete(ctx, key) if err != nil { log.Fatalf("Error trying to delete key %v", key) } } ``` ## Compatibility A **storage backend** in `valkeyrie` implements (fully or partially) the [Store](https://github.com/kvtools/valkeyrie/blob/master/store/store.go#L69) interface. | Calls | Consul | Etcd | Zookeeper | Redis | BoltDB | DynamoDB | |-----------------------|:------:|:----:|:---------:|:-----:|:------:|:--------:| | Put | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | Get | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | Delete | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | Exists | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | Watch | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🔴 | 🔴 | | WatchTree | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🔴 | 🔴 | | NewLock (Lock/Unlock) | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🔴 | 🟢️ | | List | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | DeleteTree | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | AtomicPut | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | | AtomicDelete | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | 🟢️ | The store implementations: - [boltdb](https://github.com/kvtools/boltdb) - [consul](https://github.com/kvtools/consul) - [dynamodb](https://github.com/kvtools/dynamodb) - [etcdv2](https://github.com/kvtools/etcdv2) - [etcdv3](https://github.com/kvtools/etcdv3) - [redis](https://github.com/kvtools/redis) - [zookeeper](https://github.com/kvtools/zookeeper) The store template: - [template](https://github.com/kvtools/template) ## Limitations Distributed Key/Value stores often have different concepts for managing and formatting keys and their associated values. Even though `valkeyrie` tries to abstract those stores aiming for some consistency, in some cases it can't be applied easily. Calls like `WatchTree` may return different events (or number of events) depending on the backend (for now, `Etcd` and `Consul` will likely return more events than `Zookeeper` that you should triage properly). ## Contributing Want to contribute to `valkeyrie`? Take a look at the [Contribution Guidelines](https://github.com/kvtools/valkeyrie/blob/master/CONTRIBUTING.md). The [Maintainers](https://github.com/kvtools/valkeyrie/blob/master/maintainers.md). ## Copyright and License Apache License Version 2.0 Valkeyrie started as a hard fork of the unmaintained [libkv](https://github.com/docker/libkv). ================================================ FILE: store/errors.go ================================================ package store import ( "errors" "fmt" ) var ( // ErrCallNotSupported is thrown when a method is not implemented/supported by the current backend. ErrCallNotSupported = errors.New("the current call is not supported with this backend") // ErrNotReachable is thrown when the API cannot be reached for issuing common store operations. ErrNotReachable = errors.New("api not reachable") // ErrCannotLock is thrown when there is an error acquiring a lock on a key. ErrCannotLock = errors.New("error acquiring the lock") // ErrKeyModified is thrown during an atomic operation if the index does not match the one in the store. ErrKeyModified = errors.New("unable to complete atomic operation, key modified") // ErrKeyNotFound is thrown when the key is not found in the store during a Get operation. ErrKeyNotFound = errors.New("key not found in store") // ErrPreviousNotSpecified is thrown when the previous value is not specified for an atomic operation. ErrPreviousNotSpecified = errors.New("previous K/V pair should be provided for the Atomic operation") // ErrKeyExists is thrown when the previous value exists in the case of an AtomicPut. ErrKeyExists = errors.New("previous K/V pair exists, cannot complete Atomic operation") ) // InvalidConfigurationError is thrown when the type of the configuration is not supported by a store. type InvalidConfigurationError struct { Store string Config any } func (e *InvalidConfigurationError) Error() string { return fmt.Sprintf("%s: invalid configuration type: %T", e.Store, e.Config) } // UnknownConstructorError is thrown when a requested store is not register. type UnknownConstructorError struct { Store string } func (e UnknownConstructorError) Error() string { return fmt.Sprintf("unknown constructor %q (forgotten import?)", e.Store) } ================================================ FILE: store/helpers.go ================================================ package store import ( "strings" ) // CreateEndpoints creates a list of endpoints given the right scheme. func CreateEndpoints(addrs []string, scheme string) (entries []string) { for _, addr := range addrs { entries = append(entries, scheme+"://"+addr) } return entries } // SplitKey splits the key to extract path information. func SplitKey(key string) (path []string) { if strings.Contains(key, "/") { return strings.Split(key, "/") } return []string{key} } ================================================ FILE: store/store.go ================================================ // Package store contains KV store backends. package store import ( "context" "time" ) // Store represents the backend K/V storage. // Each store should support every call listed here. // Or it couldn't be implemented as a K/V backend for valkeyrie. type Store interface { // Put a value at the specified key. Put(ctx context.Context, key string, value []byte, opts *WriteOptions) error // Get a value given its key. Get(ctx context.Context, key string, opts *ReadOptions) (*KVPair, error) // Delete the value at the specified key. Delete(ctx context.Context, key string) error // Exists Verify if a Key exists in the store. Exists(ctx context.Context, key string, opts *ReadOptions) (bool, error) // Watch for changes on a key. Watch(ctx context.Context, key string, opts *ReadOptions) (<-chan *KVPair, error) // WatchTree watches for changes on child nodes under a given directory. WatchTree(ctx context.Context, directory string, opts *ReadOptions) (<-chan []*KVPair, error) // NewLock creates a lock for a given key. // The returned Locker is not held and must be acquired with `.Lock`. // The Value is optional. NewLock(ctx context.Context, key string, opts *LockOptions) (Locker, error) // List the content of a given prefix. List(ctx context.Context, directory string, opts *ReadOptions) ([]*KVPair, error) // DeleteTree deletes a range of keys under a given directory. DeleteTree(ctx context.Context, directory string) error // AtomicPut Atomic CAS operation on a single value. // Pass previous = nil to create a new key. AtomicPut(ctx context.Context, key string, value []byte, previous *KVPair, opts *WriteOptions) (bool, *KVPair, error) // AtomicDelete Atomic delete of a single value. AtomicDelete(ctx context.Context, key string, previous *KVPair) (bool, error) // Close the store connection. Close() error } // KVPair represents {Key, Value, LastIndex} tuple. type KVPair struct { Key string Value []byte LastIndex uint64 } // WriteOptions contains optional request parameters. type WriteOptions struct { IsDir bool TTL time.Duration // If true, the client will keep the lease alive in the background // for stores that are allowing it. KeepAlive bool } // ReadOptions contains optional request parameters. type ReadOptions struct { // Consistent defines if the behavior of a Get operation is linearizable or not. // Linearizability allows us to 'see' objects based on a real-time total order // as opposed to an arbitrary order or with stale values ('inconsistent' scenario). Consistent bool } // LockOptions contains optional request parameters. type LockOptions struct { Value []byte // Optional, value to associate with the lock. TTL time.Duration // Optional, expiration ttl associated with the lock. RenewLock chan struct{} // Optional, chan used to control and stop the session ttl renewal for the lock. DeleteOnUnlock bool // If true, the value will be deleted when the lock is unlocked or expires. } // Locker provides locking mechanism on top of the store. // Similar to sync.Locker except it may return errors. type Locker interface { Lock(ctx context.Context) (<-chan struct{}, error) Unlock(ctx context.Context) error } ================================================ FILE: testsuite/suite.go ================================================ // Package testsuite the valkeyrie tests suite. package testsuite import ( "context" "strconv" "strings" "testing" "time" "github.com/kvtools/valkeyrie/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testTimeout = 60 * time.Second // RunTestCommon tests the minimal required APIs which // should be supported by all K/V backends. func RunTestCommon(t *testing.T, kv store.Store) { t.Helper() testPutGetDeleteExists(t, kv) testList(t, kv) testDeleteTree(t, kv) } // RunTestListLock tests the list output for mutexes // and checks that internal side keys are not listed. func RunTestListLock(t *testing.T, kv store.Store) { t.Helper() testListLockKey(t, kv) } // RunTestAtomic tests the Atomic operations by the K/V // backends. func RunTestAtomic(t *testing.T, kv store.Store) { t.Helper() testAtomicPut(t, kv) testAtomicPutCreate(t, kv) testAtomicPutWithSlashSuffixKey(t, kv) testAtomicDelete(t, kv) } // RunTestWatch tests the watch/monitor APIs supported // by the K/V backends. func RunTestWatch(t *testing.T, kv store.Store) { t.Helper() testWatch(t, kv) testWatchTree(t, kv) } // RunTestLock tests the KV pair Lock/Unlock APIs supported // by the K/V backends. func RunTestLock(t *testing.T, kv store.Store) { t.Helper() testLockUnlock(t, kv) } // RunTestLockTTL tests the KV pair Lock with TTL APIs supported // by the K/V backends. func RunTestLockTTL(t *testing.T, kv store.Store, backup store.Store) { t.Helper() testLockTTL(t, kv, backup) } // RunTestTTL tests the TTL functionality of the K/V backend. func RunTestTTL(t *testing.T, kv store.Store, backup store.Store) { t.Helper() testPutTTL(t, kv, backup) } func checkPairNotNil(t *testing.T, pair *store.KVPair) { t.Helper() require.NotNilf(t, pair, "pair is nil") require.NotNilf(t, pair.Value, "value is nil") } func testPutGetDeleteExists(t *testing.T, kv store.Store) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Get a not exist key should return ErrKeyNotFound. _, err := kv.Get(ctx, "testPutGetDelete_not_exist_key", nil) assert.ErrorIs(t, err, store.ErrKeyNotFound) value := []byte("bar") for _, key := range []string{ "testPutGetDeleteExists", "testPutGetDeleteExists/", "testPutGetDeleteExists/testbar/", "testPutGetDeleteExists/testbar/testfoobar", } { // Put the key. err = kv.Put(ctx, key, value, nil) require.NoError(t, err) // Get should return the value and an incremented index. pair, err := kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) // Exists should return true. exists, err := kv.Exists(ctx, key, nil) require.NoError(t, err) assert.True(t, exists) // Delete the key. err = kv.Delete(ctx, key) require.NoError(t, err) // Get should fail. pair, err = kv.Get(ctx, key, nil) assert.Error(t, err) assert.Nil(t, pair) // Exists should return false. exists, err = kv.Exists(ctx, key, nil) require.NoError(t, err) assert.False(t, exists) } } func testWatch(t *testing.T, kv store.Store) { t.Helper() key := "testWatch" value := []byte("world") newValue := []byte("world!") ctx, cancel := context.WithTimeout(context.Background(), testTimeout) t.Cleanup(cancel) // Put the key. err := kv.Put(ctx, key, value, nil) require.NoError(t, err) events, err := kv.Watch(ctx, key, nil) require.NoError(t, err) require.NotNil(t, events) // Update loop. go func() { timeout := time.After(1 * time.Second) tick := time.NewTicker(250 * time.Millisecond) defer tick.Stop() for { select { case <-timeout: return case <-tick.C: err := kv.Put(ctx, key, newValue, nil) if assert.NoError(t, err) { continue } return } } }() // Check for updates. eventCount := 1 for { select { case event := <-events: assert.NotNil(t, event) assert.Equal(t, event.Key, key) if eventCount == 1 { assert.Equal(t, event.Value, value) } else { assert.Equal(t, event.Value, newValue) } eventCount++ // We received all the events we wanted to check. if eventCount >= 4 { return } case <-time.After(4 * time.Second): t.Fatal("Timeout reached") return } } } func testWatchTree(t *testing.T, kv store.Store) { t.Helper() dir := "testWatchTree" node1 := "testWatchTree/node1" value1 := []byte("node1") node2 := "testWatchTree/node2" value2 := []byte("node2") node3 := "testWatchTree/node3" value3 := []byte("node3") ctx, cancel := context.WithTimeout(context.Background(), testTimeout) t.Cleanup(cancel) err := kv.Put(ctx, node1, value1, nil) require.NoError(t, err) err = kv.Put(ctx, node2, value2, nil) require.NoError(t, err) err = kv.Put(ctx, node3, value3, nil) require.NoError(t, err) events, err := kv.WatchTree(ctx, dir, nil) require.NoError(t, err) require.NotNil(t, events) // Update loop. go func() { time.Sleep(500 * time.Millisecond) err := kv.Delete(ctx, node3) require.NoError(t, err) }() // Check for updates. eventCount := 1 for { select { case event := <-events: assert.NotNil(t, event) // We received the Delete event on a child node // Exit test successfully. if eventCount == 2 { return } eventCount++ case <-time.After(4 * time.Second): t.Fatal("Timeout reached") return } } } func testAtomicPut(t *testing.T, kv store.Store) { t.Helper() key := "testAtomicPut" value := []byte("world") ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Put the key. err := kv.Put(ctx, key, value, nil) require.NoError(t, err) // Get should return the value and an incremented index. pair, err := kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) // This CAS should fail: previous exists. success, _, err := kv.AtomicPut(ctx, key, []byte("WORLD"), nil, nil) assert.Error(t, err) assert.False(t, success) // This CAS should succeed. success, _, err = kv.AtomicPut(ctx, key, []byte("WORLD"), pair, nil) require.NoError(t, err) assert.True(t, success) // This CAS should fail, key has wrong index. pair.LastIndex = 6744 success, _, err = kv.AtomicPut(ctx, key, []byte("WORLDWORLD"), pair, nil) assert.Equal(t, err, store.ErrKeyModified) assert.False(t, success) } func testAtomicPutCreate(t *testing.T, kv store.Store) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Use a key in a new directory to ensure Stores will create directories // that don't yet exist. key := "testAtomicPutCreate/create" value := []byte("putcreate") // AtomicPut the key, previous = nil indicates create. success, _, err := kv.AtomicPut(ctx, key, value, nil, nil) require.NoError(t, err) assert.True(t, success) // Get should return the value and an incremented index. pair, err := kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) // Attempting to create again should fail. success, _, err = kv.AtomicPut(ctx, key, value, nil, nil) assert.ErrorIs(t, err, store.ErrKeyExists) assert.False(t, success) // This CAS should succeed, since it has the value from Get(). success, _, err = kv.AtomicPut(ctx, key, []byte("PUTCREATE"), pair, nil) require.NoError(t, err) assert.True(t, success) } func testAtomicPutWithSlashSuffixKey(t *testing.T, kv store.Store) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() k1 := "testAtomicPutWithSlashSuffixKey/key/" success, _, err := kv.AtomicPut(ctx, k1, []byte{}, nil, nil) require.NoError(t, err) assert.True(t, success) } func testAtomicDelete(t *testing.T, kv store.Store) { t.Helper() key := "testAtomicDelete" value := []byte("world") ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Put the key. err := kv.Put(ctx, key, value, nil) require.NoError(t, err) // Get should return the value and an incremented index. pair, err := kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) tempIndex := pair.LastIndex // AtomicDelete should fail. pair.LastIndex = 6744 success, err := kv.AtomicDelete(ctx, key, pair) assert.Error(t, err) assert.False(t, success) // AtomicDelete should succeed. pair.LastIndex = tempIndex success, err = kv.AtomicDelete(ctx, key, pair) require.NoError(t, err) assert.True(t, success) // Delete a non-existent key; should fail. success, err = kv.AtomicDelete(ctx, key, pair) assert.ErrorIs(t, err, store.ErrKeyNotFound) assert.False(t, success) } func testLockUnlock(t *testing.T, kv store.Store) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() key := "testLockUnlock" value := []byte("bar") // We should be able to create a new lock on key. lock, err := kv.NewLock(ctx, key, &store.LockOptions{ Value: value, TTL: 2 * time.Second, DeleteOnUnlock: true, }) require.NoError(t, err) require.NotNil(t, lock) // Lock should successfully succeed or block. lockChan, err := lock.Lock(ctx) require.NoError(t, err) assert.NotNil(t, lockChan) // Get should work. pair, err := kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) // Unlock should succeed. err = lock.Unlock(ctx) require.NoError(t, err) // Lock should succeed again. lockChan, err = lock.Lock(ctx) require.NoError(t, err) assert.NotNil(t, lockChan) // Get should work. pair, err = kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) err = lock.Unlock(ctx) require.NoError(t, err) } func testLockTTL(t *testing.T, kv store.Store, otherConn store.Store) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() key := "testLockTTL" value := []byte("bar") renewCh := make(chan struct{}) // We should be able to create a new lock on key. lockOC, err := otherConn.NewLock(ctx, key, &store.LockOptions{ Value: value, TTL: 2 * time.Second, RenewLock: renewCh, }) require.NoError(t, err) require.NotNil(t, lockOC) // Lock should successfully succeed. lockChan, err := lockOC.Lock(ctx) require.NoError(t, err) assert.NotNil(t, lockChan) // Get should work. pair, err := otherConn.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) time.Sleep(3 * time.Second) value = []byte("foobar") // Create a new lock with another connection. lock, err := kv.NewLock(ctx, key, &store.LockOptions{ Value: value, TTL: 3 * time.Second, }) require.NoError(t, err) require.NotNil(t, lock) ctxLock, cancelLock := context.WithTimeout(ctx, 4*time.Second) defer cancelLock() // Lock should block, the session on the lock // is still active and renewed periodically. lockChan, _ = lock.Lock(ctxLock) require.Nil(t, lockChan) // Close the connection. _ = otherConn.Close() // Force to stop the session renewal for the lock. close(renewCh) // Let the session on the lock expire. time.Sleep(3 * time.Second) // Lock should now succeed for the other client. locked := make(chan struct{}) go func(<-chan struct{}) { lockChan, err = lock.Lock(ctx) require.NoError(t, err) assert.NotNil(t, lockChan) locked <- struct{}{} }(locked) select { case <-locked: break case <-time.After(4 * time.Second): t.Fatal("Unable to take the lock, timed out") } // Get should work with the new value. pair, err = kv.Get(ctx, key, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, value) assert.NotEqual(t, pair.LastIndex, 0) err = lock.Unlock(ctx) require.NoError(t, err) } func testPutTTL(t *testing.T, kv store.Store, otherConn store.Store) { t.Helper() firstKey := "testPutTTL" firstValue := []byte("foo") secondKey := "second" secondValue := []byte("bar") ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Put the first key with the Ephemeral flag. err := otherConn.Put(ctx, firstKey, firstValue, &store.WriteOptions{TTL: 2 * time.Second}) require.NoError(t, err) // Put a second key with the Ephemeral flag. err = otherConn.Put(ctx, secondKey, secondValue, &store.WriteOptions{TTL: 2 * time.Second}) require.NoError(t, err) // Get on firstKey should work. pair, err := kv.Get(ctx, firstKey, nil) require.NoError(t, err) checkPairNotNil(t, pair) // Get on secondKey should work. pair, err = kv.Get(ctx, secondKey, nil) require.NoError(t, err) checkPairNotNil(t, pair) // Close the connection. _ = otherConn.Close() // Let the session expire. time.Sleep(3 * time.Second) // Get on firstKey shouldn't work. pair, err = kv.Get(ctx, firstKey, nil) assert.Error(t, err) assert.Nil(t, pair) // Get on secondKey shouldn't work. pair, err = kv.Get(ctx, secondKey, nil) assert.Error(t, err) assert.Nil(t, pair) } func testList(t *testing.T, kv store.Store) { t.Helper() parentKey := "testList" childKey := "testList/child" subfolderKey := "testList/subfolder" ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Put the parent key. err := kv.Put(ctx, parentKey, nil, &store.WriteOptions{IsDir: true}) require.NoError(t, err) // Put the first child key. err = kv.Put(ctx, childKey, []byte("first"), nil) require.NoError(t, err) // Put the second child key which is also a directory. err = kv.Put(ctx, subfolderKey, []byte("second"), &store.WriteOptions{IsDir: true}) require.NoError(t, err) // Put child keys under secondKey. for i := 1; i <= 3; i++ { key := "testList/subfolder/key" + strconv.Itoa(i) err = kv.Put(ctx, key, []byte("value"), nil) require.NoError(t, err) } // List should work and return five child entries. for _, parent := range []string{parentKey, parentKey + "/"} { pairs, errList := kv.List(ctx, parent, nil) require.NoError(t, errList) assert.Len(t, pairs, 5) } // List on childKey should return 0 keys. pairs, err := kv.List(ctx, childKey, nil) require.NoError(t, err) assert.Empty(t, pairs) // List on subfolderKey should return 3 keys without the directory. pairs, err = kv.List(ctx, subfolderKey, nil) require.NoError(t, err) assert.Len(t, pairs, 3) // List should fail: the key does not exist. pairs, err = kv.List(ctx, "idontexist", nil) assert.ErrorIs(t, err, store.ErrKeyNotFound) assert.Nil(t, pairs) } func testListLockKey(t *testing.T, kv store.Store) { t.Helper() listKey := "testListLockSide" ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := kv.Put(ctx, listKey, []byte("val"), &store.WriteOptions{IsDir: true}) require.NoError(t, err) err = kv.Put(ctx, listKey+"/subfolder", []byte("val"), &store.WriteOptions{IsDir: true}) require.NoError(t, err) // Put keys under subfolder. for i := 1; i <= 3; i++ { key := listKey + "/subfolder/key" + strconv.Itoa(i) errPut := kv.Put(ctx, key, []byte("val"), nil) require.NoError(t, errPut) // We lock the child key. lock, errPut := kv.NewLock(ctx, key, &store.LockOptions{Value: []byte("locked"), TTL: 2 * time.Second}) require.NoError(t, errPut) require.NotNil(t, lock) lockChan, errPut := lock.Lock(ctx) require.NoError(t, errPut) assert.NotNil(t, lockChan) } // List children of the root directory (`listKey`), this should // not output any `___lock` entries and must contain 4 results. pairs, err := kv.List(ctx, listKey, nil) require.NoError(t, err) assert.Len(t, pairs, 4) for _, pair := range pairs { if strings.Contains(pair.Key, "___lock") { assert.FailNow(t, "tesListLockKey: found a key containing lock suffix '___lock'") } } } func testDeleteTree(t *testing.T, kv store.Store) { t.Helper() prefix := "testDeleteTree" firstKey := "testDeleteTree/first" firstValue := []byte("first") secondKey := "testDeleteTree/second" secondValue := []byte("second") ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Put the first key. err := kv.Put(ctx, firstKey, firstValue, nil) require.NoError(t, err) // Put the second key. err = kv.Put(ctx, secondKey, secondValue, nil) require.NoError(t, err) // Get should work on the first Key. pair, err := kv.Get(ctx, firstKey, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, firstValue) assert.NotEqual(t, 0, pair.LastIndex) // Get should work on the second Key. pair, err = kv.Get(ctx, secondKey, nil) require.NoError(t, err) checkPairNotNil(t, pair) assert.Equal(t, pair.Value, secondValue) assert.NotEqual(t, 0, pair.LastIndex) // Delete Values under directory `nodes`. err = kv.DeleteTree(ctx, prefix) require.NoError(t, err) // Get should fail on both keys. pair, err = kv.Get(ctx, firstKey, nil) assert.Error(t, err) assert.Nil(t, pair) pair, err = kv.Get(ctx, secondKey, nil) assert.Error(t, err) assert.Nil(t, pair) } // RunCleanup cleans up keys introduced by the tests. func RunCleanup(t *testing.T, kv store.Store) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() for _, key := range []string{ "testAtomicPutWithSlashSuffixKey", "testPutGetDeleteExists", "testWatch", "testWatchTree", "testAtomicPut", "testAtomicPutCreate", "testAtomicDelete", "testLockUnlock", "testLockTTL", "testPutTTL", "testList/subfolder", "testList", "testListLockSide/subfolder", "testListLockSide", "testDeleteTree", } { err := kv.DeleteTree(ctx, key) if err != nil { assert.ErrorIsf(t, err, store.ErrKeyNotFound, "failed to delete tree key %s", key) } err = kv.Delete(ctx, key) if err != nil { assert.ErrorIsf(t, err, store.ErrKeyNotFound, "failed to delete key %s", key) } } } ================================================ FILE: valkeyrie.go ================================================ // Package valkeyrie Distributed Key/Value Store Abstraction Library written in Go. package valkeyrie import ( "context" "sort" "sync" "github.com/kvtools/valkeyrie/store" ) var ( constructorsMu sync.RWMutex constructors = make(map[string]Constructor) ) // Config the raw type of the store configurations. type Config any // Constructor The signature of a store constructor. type Constructor func(ctx context.Context, endpoints []string, options Config) (store.Store, error) // Register makes a store constructor available by the provided name. // If Register is called twice with the same name or if constructor is nil, it panics. func Register(name string, cttr Constructor) { constructorsMu.Lock() defer constructorsMu.Unlock() if cttr == nil { panic("valkeyrie: Register constructor is nil") } if _, dup := constructors[name]; dup { panic("valkeyrie: Register called twice for constructor " + name) } constructors[name] = cttr } // Unregister Unregisters a store. func Unregister(storeName string) { constructorsMu.Lock() defer constructorsMu.Unlock() delete(constructors, storeName) } // UnregisterAllConstructors Unregisters all stores. func UnregisterAllConstructors() { constructorsMu.Lock() defer constructorsMu.Unlock() constructors = make(map[string]Constructor) } // Constructors returns a sorted list of the names of the registered constructors. func Constructors() []string { constructorsMu.RLock() defer constructorsMu.RUnlock() list := make([]string, 0, len(constructors)) for name := range constructors { list = append(list, name) } sort.Strings(list) return list } // NewStore creates a new store instance. func NewStore(ctx context.Context, storeName string, endpoints []string, options Config) (store.Store, error) { constructorsMu.RLock() construct, ok := constructors[storeName] constructorsMu.RUnlock() if !ok { return nil, &store.UnknownConstructorError{Store: storeName} } if construct == nil { return nil, &store.UnknownConstructorError{Store: storeName} } return construct(ctx, endpoints, options) } ================================================ FILE: valkeyrie_test.go ================================================ package valkeyrie import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRegister(t *testing.T) { t.Cleanup(UnregisterAllConstructors) Register(testStoreName, newStore) assert.Len(t, constructors, 1) } func TestRegister_duplicate(t *testing.T) { t.Cleanup(UnregisterAllConstructors) Register(testStoreName, newStore) assert.Len(t, constructors, 1) assert.Panics(t, func() { Register(testStoreName, newStore) }) } func TestRegister_nil(t *testing.T) { t.Cleanup(UnregisterAllConstructors) assert.Panics(t, func() { Register(testStoreName, nil) }) } func TestUnregister(t *testing.T) { t.Cleanup(UnregisterAllConstructors) Register(testStoreName, newStore) assert.Len(t, constructors, 1) Unregister(testStoreName) constructorsMu.Lock() defer constructorsMu.Unlock() assert.Empty(t, constructors) } func TestConstructors(t *testing.T) { t.Cleanup(UnregisterAllConstructors) Register(testStoreName, newStore) assert.Len(t, constructors, 1) cttrs := Constructors() expected := []string{testStoreName} assert.Equal(t, expected, cttrs) } func TestNewStore(t *testing.T) { t.Cleanup(UnregisterAllConstructors) Register(testStoreName, newStore) assert.Len(t, constructors, 1) s, err := NewStore(context.Background(), testStoreName, nil, nil) require.NoError(t, err) assert.NotNil(t, s) assert.IsType(t, &Mock{}, s) }