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: |-
<details>
```console
$ go version && go env
# paste output here
```
</details>
validations:
required: true
- type: textarea
id: code-example
attributes:
label: Code example or link to a public repository
value: |-
<details>
```go
// add your code here
```
</details>
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
================================================
<p align="center">
<img alt="golangci-lint logo" src="docs/valkeyrie.png" height="350" />
<h3 align="center">Valkeyrie</h3>
<p align="center">Distributed Key/Value Store Abstraction Library</p>
</p>
# Valkeyrie
[](https://pkg.go.dev/github.com/kvtools/valkeyrie)
[](https://github.com/kvtools/valkeyrie/actions/workflows/build.yml)
[](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)
}
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
SYMBOL INDEX (64 symbols across 7 files)
FILE: mock_test.go
constant testStoreName (line 9) | testStoreName = "mock"
function newStore (line 11) | func newStore(ctx context.Context, endpoints []string, options Config) (...
type Mock (line 20) | type Mock struct
method Put (line 31) | func (m Mock) Put(_ context.Context, _ string, _ []byte, _ *store.Writ...
method Get (line 35) | func (m Mock) Get(_ context.Context, _ string, _ *store.ReadOptions) (...
method Delete (line 39) | func (m Mock) Delete(_ context.Context, _ string) error {
method Exists (line 43) | func (m Mock) Exists(_ context.Context, _ string, _ *store.ReadOptions...
method Watch (line 47) | func (m Mock) Watch(_ context.Context, _ string, _ *store.ReadOptions)...
method WatchTree (line 51) | func (m Mock) WatchTree(_ context.Context, _ string, _ *store.ReadOpti...
method NewLock (line 55) | func (m Mock) NewLock(_ context.Context, _ string, _ *store.LockOption...
method List (line 59) | func (m Mock) List(_ context.Context, _ string, _ *store.ReadOptions) ...
method DeleteTree (line 63) | func (m Mock) DeleteTree(_ context.Context, _ string) error {
method AtomicPut (line 67) | func (m Mock) AtomicPut(_ context.Context, _ string, _ []byte, _ *stor...
method AtomicDelete (line 71) | func (m Mock) AtomicDelete(_ context.Context, _ string, _ *store.KVPai...
method Close (line 75) | func (m Mock) Close() error {
function New (line 27) | func New(_ context.Context, _ []string, cfg *Config) (*Mock, error) {
FILE: store/errors.go
type InvalidConfigurationError (line 26) | type InvalidConfigurationError struct
method Error (line 31) | func (e *InvalidConfigurationError) Error() string {
type UnknownConstructorError (line 36) | type UnknownConstructorError struct
method Error (line 40) | func (e UnknownConstructorError) Error() string {
FILE: store/helpers.go
function CreateEndpoints (line 8) | func CreateEndpoints(addrs []string, scheme string) (entries []string) {
function SplitKey (line 17) | func SplitKey(key string) (path []string) {
FILE: store/store.go
type Store (line 12) | type Store interface
type KVPair (line 54) | type KVPair struct
type WriteOptions (line 61) | type WriteOptions struct
type ReadOptions (line 71) | type ReadOptions struct
type LockOptions (line 79) | type LockOptions struct
type Locker (line 88) | type Locker interface
FILE: testsuite/suite.go
constant testTimeout (line 16) | testTimeout = 60 * time.Second
function RunTestCommon (line 20) | func RunTestCommon(t *testing.T, kv store.Store) {
function RunTestListLock (line 30) | func RunTestListLock(t *testing.T, kv store.Store) {
function RunTestAtomic (line 38) | func RunTestAtomic(t *testing.T, kv store.Store) {
function RunTestWatch (line 49) | func RunTestWatch(t *testing.T, kv store.Store) {
function RunTestLock (line 58) | func RunTestLock(t *testing.T, kv store.Store) {
function RunTestLockTTL (line 66) | func RunTestLockTTL(t *testing.T, kv store.Store, backup store.Store) {
function RunTestTTL (line 73) | func RunTestTTL(t *testing.T, kv store.Store, backup store.Store) {
function checkPairNotNil (line 79) | func checkPairNotNil(t *testing.T, pair *store.KVPair) {
function testPutGetDeleteExists (line 86) | func testPutGetDeleteExists(t *testing.T, kv store.Store) {
function testWatch (line 135) | func testWatch(t *testing.T, kv store.Store) {
function testWatchTree (line 196) | func testWatchTree(t *testing.T, kv store.Store) {
function testAtomicPut (line 251) | func testAtomicPut(t *testing.T, kv store.Store) {
function testAtomicPutCreate (line 288) | func testAtomicPutCreate(t *testing.T, kv store.Store) {
function testAtomicPutWithSlashSuffixKey (line 321) | func testAtomicPutWithSlashSuffixKey(t *testing.T, kv store.Store) {
function testAtomicDelete (line 333) | func testAtomicDelete(t *testing.T, kv store.Store) {
function testLockUnlock (line 373) | func testLockUnlock(t *testing.T, kv store.Store) {
function testLockTTL (line 423) | func testLockTTL(t *testing.T, kv store.Store, otherConn store.Store) {
function testPutTTL (line 511) | func testPutTTL(t *testing.T, kv store.Store, otherConn store.Store) {
function testList (line 558) | func testList(t *testing.T, kv store.Store) {
function testListLockKey (line 610) | func testListLockKey(t *testing.T, kv store.Store) {
function testDeleteTree (line 653) | func testDeleteTree(t *testing.T, kv store.Store) {
function RunCleanup (line 704) | func RunCleanup(t *testing.T, kv store.Store) {
FILE: valkeyrie.go
type Config (line 18) | type Config
type Constructor (line 21) | type Constructor
function Register (line 25) | func Register(name string, cttr Constructor) {
function Unregister (line 41) | func Unregister(storeName string) {
function UnregisterAllConstructors (line 49) | func UnregisterAllConstructors() {
function Constructors (line 57) | func Constructors() []string {
function NewStore (line 72) | func NewStore(ctx context.Context, storeName string, endpoints []string,...
FILE: valkeyrie_test.go
function TestRegister (line 11) | func TestRegister(t *testing.T) {
function TestRegister_duplicate (line 19) | func TestRegister_duplicate(t *testing.T) {
function TestRegister_nil (line 30) | func TestRegister_nil(t *testing.T) {
function TestUnregister (line 38) | func TestUnregister(t *testing.T) {
function TestConstructors (line 52) | func TestConstructors(t *testing.T) {
function TestNewStore (line 64) | func TestNewStore(t *testing.T) {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (67K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1195,
"preview": "name: 🐞 Bug Report\ndescription: Create a report to help us improve.\nbody:\n - type: checkboxes\n id: terms\n attribu"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 224,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: ❓ Questions\n url: https://github.com/kvtools/valkeyrie/discussio"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1054,
"preview": "name: 💡 Feature request\ndescription: \"Suggest an idea for this project.\"\nbody:\n\n - type: textarea\n id: problem\n a"
},
{
"path": ".github/ISSUE_TEMPLATE/new_store.yml",
"chars": 516,
"preview": "name: 🧩 Add a new type of store\ndescription: \"Proposal\"\nbody:\n\n - type: input\n id: store-name\n attributes:\n "
},
{
"path": ".github/dependabot.yml",
"chars": 510,
"preview": "version: 2\nupdates:\n - package-ecosystem: gomod\n directory: \"/\"\n schedule:\n interval: monthly\n cooldown:\n"
},
{
"path": ".github/workflows/build.yml",
"chars": 726,
"preview": "name: Build and test\n\non: [push, pull_request]\n\nenv:\n GOLANGCI_LINT_VERSION: v2.11\n\npermissions: {}\n\njobs:\n build:\n "
},
{
"path": ".gitignore",
"chars": 32,
"preview": ".idea/\n.DS_Store\n*.iml\ndump.rdb\n"
},
{
"path": ".golangci.yml",
"chars": 2201,
"preview": "version: \"2\"\n\nformatters:\n enable:\n - gci\n - gofumpt\n settings:\n gofumpt:\n extra-rules: false\n\nlinters:\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 3633,
"preview": "# Contributing\n\nAll contributions are useful, whether it is a simple typo, a more complex change, or just pointing out a"
},
{
"path": "LICENSE",
"chars": 10804,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 152,
"preview": ".PHONY: all\nall: validate test\n\n## Run validates\n.PHONY: validate\nvalidate:\n\tgolangci-lint run\n\n## Run tests\n.PHONY: tes"
},
{
"path": "docs/examples.md",
"chars": 4532,
"preview": "# Examples\n\nThis document contains useful example of usage for `valkeyrie`.\nIt might not be complete but provides with g"
},
{
"path": "go.mod",
"chars": 344,
"preview": "module github.com/kvtools/valkeyrie\n\ngo 1.22\n\nrequire github.com/stretchr/testify v1.11.1\n\nrequire (\n\tgithub.com/davecgh"
},
{
"path": "go.sum",
"chars": 1680,
"preview": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.1 h1"
},
{
"path": "maintainers.md",
"chars": 337,
"preview": "- [Alexandre Beslic](https://github.com/abronan)\n- [Victor Castell](https://github.com/victorcoder)\n- [Nicolas Mengin](h"
},
{
"path": "mock_test.go",
"chars": 1941,
"preview": "package valkeyrie\n\nimport (\n\t\"context\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n)\n\nconst testStoreName = \"mock\"\n\nfunc newS"
},
{
"path": "readme.md",
"chars": 4664,
"preview": "<p align=\"center\">\n <img alt=\"golangci-lint logo\" src=\"docs/valkeyrie.png\" height=\"350\" />\n <h3 align=\"center\">Valkeyr"
},
{
"path": "store/errors.go",
"chars": 1809,
"preview": "package store\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\nvar (\n\t// ErrCallNotSupported is thrown when a method is not implemented/sup"
},
{
"path": "store/helpers.go",
"chars": 476,
"preview": "package store\n\nimport (\n\t\"strings\"\n)\n\n// CreateEndpoints creates a list of endpoints given the right scheme.\nfunc Create"
},
{
"path": "store/store.go",
"chars": 3266,
"preview": "// Package store contains KV store backends.\npackage store\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// Store represents the backe"
},
{
"path": "testsuite/suite.go",
"chars": 18285,
"preview": "// Package testsuite the valkeyrie tests suite.\npackage testsuite\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n"
},
{
"path": "valkeyrie.go",
"chars": 2094,
"preview": "// Package valkeyrie Distributed Key/Value Store Abstraction Library written in Go.\npackage valkeyrie\n\nimport (\n\t\"contex"
},
{
"path": "valkeyrie_test.go",
"chars": 1431,
"preview": "package valkeyrie\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/r"
}
]
About this extraction
This page contains the full source code of the abronan/valkeyrie GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (60.5 KB), approximately 16.8k tokens, and a symbol index with 64 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.