├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── build.yml │ └── codeql.yml ├── .gitignore ├── .golangci.yml ├── .vscode └── launch.json ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── backend ├── backend.go ├── gcs │ └── gcs.go ├── memory │ └── memory.go └── middleware │ ├── backendlogger.go │ ├── delaybackend.go │ └── delaybackend_test.go ├── bench_test.go ├── codecov.yml ├── collection.go ├── db.go ├── demo └── demo.go ├── docs ├── bugs.md ├── img │ ├── deadlock-latency.png │ ├── ops-latency.png │ ├── retries.png │ ├── tx-latency.png │ └── tx-throughput.png └── profile │ ├── bench-2023-09-02.txt │ ├── bench-2023-09-05.txt │ ├── bench-2023-09-06.txt │ ├── bench-2023-09-15.txt │ └── bench-2024-06-23.txt ├── glassdb_test.go ├── go.mod ├── go.sum ├── hack ├── backendbench │ └── backendbench.go ├── benchstat-cmp.sh ├── benchstat.sh ├── debug │ ├── debug.go │ └── logs2csv.sh ├── pprof.sh ├── rtbench │ └── rtbench.go └── test-all.sh ├── internal ├── cache │ ├── cache.go │ └── cache_test.go ├── concurr │ ├── background.go │ ├── background_test.go │ ├── backoff.go │ ├── chan.go │ ├── chan_test.go │ ├── context.go │ ├── context_test.go │ ├── dedup.go │ ├── dedup_test.go │ ├── fanout.go │ └── fanout_test.go ├── data │ ├── paths │ │ └── paths.go │ └── txid.go ├── errors │ ├── err.go │ └── err_test.go ├── proto │ ├── proto.go │ ├── transaction.pb.go │ └── transaction.proto ├── storage │ ├── global.go │ ├── local.go │ ├── locker.go │ ├── tlogger.go │ ├── tlogger_test.go │ └── version.go ├── stringset │ └── stringset.go ├── testkit │ ├── bench │ │ └── bench.go │ ├── clock.go │ ├── clock_test.go │ ├── fake_gcs_client.go │ ├── fake_gcs_server.go │ ├── fake_gcs_service.go │ ├── fake_service_test.go │ ├── race.go │ ├── race_no.go │ ├── request.go │ ├── response.go │ ├── slogger.go │ └── stall_backend.go ├── trace │ ├── notrace.go │ └── trace.go └── trans │ ├── algo.go │ ├── algo_test.go │ ├── gc.go │ ├── gc_test.go │ ├── monitor.go │ ├── monitor_test.go │ ├── reader.go │ ├── tlocker.go │ └── tlocker_test.go ├── iter.go ├── stats.go ├── tx.go ├── version.go └── version_test.go /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The glassdb Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG VARIANT=1.21 16 | FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT}-bookworm 17 | 18 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \ 19 | apt-get -y install --no-install-recommends \ 20 | protobuf-compiler libprotobuf-dev graphviz 21 | 22 | USER vscode 23 | RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ 24 | go install golang.org/x/perf/cmd/benchstat@latest 25 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json 2 | { 3 | "name": "Go", 4 | "build": { 5 | "dockerfile": "Dockerfile" 6 | }, 7 | 8 | // Configure tool-specific properties. 9 | "customizations": { 10 | // Configure properties specific to VS Code. 11 | "vscode": { 12 | // Add the IDs of extensions you want installed when the container is created. 13 | "extensions": [ 14 | "golang.Go" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: main 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 1.21 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.21 23 | 24 | - name: Test 25 | run: ./hack/test-all.sh 26 | 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v6 29 | with: 30 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 31 | version: v1.54 32 | 33 | - name: Coverage 34 | run: go test -coverprofile=coverage.txt -covermode=atomic ./... 35 | 36 | - name: Upload coverage reports to Codecov 37 | uses: codecov/codecov-action@v5 38 | with: 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "main" ] 9 | schedule: 10 | - cron: '26 13 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: 'ubuntu-latest' 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'go' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v3 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{matrix.language}}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env/ 2 | /docs/profile/*.prof 3 | /docs/profile/trace.out 4 | /glassdb.test 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The glassdb Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | run: 16 | deadline: "3m" 17 | 18 | linters: 19 | enable-all: false 20 | enable: 21 | - asciicheck 22 | - bidichk 23 | - errcheck 24 | - errname 25 | - errorlint 26 | - exportloopref 27 | - gocritic 28 | - gofmt 29 | - gosec 30 | - gosimple 31 | - govet 32 | - ineffassign 33 | - misspell 34 | - revive 35 | - staticcheck 36 | - unconvert 37 | - unused 38 | 39 | linters-settings: 40 | errorlint: 41 | errorf: false 42 | 43 | revive: 44 | rules: 45 | - name: "empty-block" 46 | disabled: true 47 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug (rr)", 6 | "type": "go", 7 | "backend": "rr", 8 | "request": "launch", 9 | "mode": "auto", 10 | "program": "${fileDirname}" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of [Project Name]'s significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | 8 | Google LLC 9 | Michele Bertasi 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to mbrt@ (michele dot bertasi at gmail dot com), the 73 | Project Steward(s) for GlassDB. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 94 | -------------------------------------------------------------------------------- /backend/backend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package backend 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | ) 21 | 22 | var ( 23 | ErrNotFound = errors.New("object not found") 24 | ErrPrecondition = errors.New("precondition failed") 25 | ErrEOF = errors.New("end of file") 26 | ) 27 | 28 | type Backend interface { 29 | ReadIfModified(ctx context.Context, path string, version int64) (ReadReply, error) 30 | Read(ctx context.Context, path string) (ReadReply, error) 31 | GetMetadata(ctx context.Context, path string) (Metadata, error) 32 | SetTagsIf(ctx context.Context, path string, expected Version, t Tags) (Metadata, error) 33 | Write(ctx context.Context, path string, value []byte, t Tags) (Metadata, error) 34 | WriteIf(ctx context.Context, path string, value []byte, expected Version, t Tags) (Metadata, error) 35 | WriteIfNotExists(ctx context.Context, path string, value []byte, t Tags) (Metadata, error) 36 | Delete(ctx context.Context, path string) error 37 | DeleteIf(ctx context.Context, path string, expected Version) error 38 | 39 | // List returns an iterator over the objects in the bucket within the given 40 | // directory (path separator is '/'). Objects will be iterated over 41 | // lexicographically by name. 42 | // 43 | // Note: The returned iterator is not safe for concurrent operations without 44 | // explicit synchronization. 45 | List(ctx context.Context, dirPath string) (ListIter, error) 46 | } 47 | 48 | type ListIter interface { 49 | Next() (path string, ok bool) 50 | Err() error 51 | } 52 | 53 | type ReadReply struct { 54 | Contents []byte 55 | // TODO: Consider only exposing contents version. 56 | Version Version 57 | } 58 | 59 | type Tags map[string]string 60 | 61 | type Metadata struct { 62 | Tags Tags 63 | Version Version 64 | } 65 | 66 | type Version struct { 67 | Contents int64 68 | Meta int64 69 | } 70 | 71 | func (v Version) IsNull() bool { 72 | return v.Contents == 0 73 | } 74 | -------------------------------------------------------------------------------- /backend/memory/memory.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package memory 16 | 17 | import ( 18 | "context" 19 | "sort" 20 | "strings" 21 | "sync" 22 | 23 | "github.com/mbrt/glassdb/backend" 24 | "github.com/mbrt/glassdb/internal/stringset" 25 | ) 26 | 27 | func New() *Backend { 28 | return &Backend{ 29 | objects: make(map[string]object), 30 | nextGen: 1, 31 | } 32 | } 33 | 34 | type Backend struct { 35 | objects map[string]object 36 | nextGen int64 37 | m sync.Mutex 38 | } 39 | 40 | func (b *Backend) ReadIfModified( 41 | ctx context.Context, 42 | path string, 43 | version int64, 44 | ) (backend.ReadReply, error) { 45 | if err := ctx.Err(); err != nil { 46 | return backend.ReadReply{}, err 47 | } 48 | 49 | b.m.Lock() 50 | defer b.m.Unlock() 51 | 52 | obj, ok := b.objects[path] 53 | if !ok { 54 | return backend.ReadReply{}, backend.ErrNotFound 55 | } 56 | if obj.Version.Contents == version { 57 | return backend.ReadReply{}, backend.ErrPrecondition 58 | } 59 | return backend.ReadReply{ 60 | Contents: obj.Data, 61 | Version: obj.Version, 62 | }, nil 63 | } 64 | 65 | func (b *Backend) Read(ctx context.Context, path string) (backend.ReadReply, error) { 66 | if err := ctx.Err(); err != nil { 67 | return backend.ReadReply{}, err 68 | } 69 | 70 | b.m.Lock() 71 | defer b.m.Unlock() 72 | 73 | obj, ok := b.objects[path] 74 | if !ok { 75 | return backend.ReadReply{}, backend.ErrNotFound 76 | } 77 | return backend.ReadReply{ 78 | Contents: obj.Data, 79 | Version: obj.Version, 80 | }, nil 81 | } 82 | 83 | func (b *Backend) GetMetadata( 84 | ctx context.Context, 85 | path string, 86 | ) (backend.Metadata, error) { 87 | if err := ctx.Err(); err != nil { 88 | return backend.Metadata{}, err 89 | } 90 | 91 | b.m.Lock() 92 | defer b.m.Unlock() 93 | 94 | obj, ok := b.objects[path] 95 | if !ok { 96 | return backend.Metadata{}, backend.ErrNotFound 97 | } 98 | return backend.Metadata{ 99 | Tags: copyTags(obj.Tags), 100 | Version: obj.Version, 101 | }, nil 102 | } 103 | 104 | func (b *Backend) SetTagsIf( 105 | ctx context.Context, 106 | path string, 107 | expected backend.Version, 108 | t backend.Tags, 109 | ) (backend.Metadata, error) { 110 | if err := ctx.Err(); err != nil { 111 | return backend.Metadata{}, err 112 | } 113 | 114 | b.m.Lock() 115 | defer b.m.Unlock() 116 | 117 | obj, ok := b.objects[path] 118 | if !ok { 119 | return backend.Metadata{}, backend.ErrNotFound 120 | } 121 | if obj.Version != expected { 122 | return backend.Metadata{}, backend.ErrPrecondition 123 | } 124 | b.updateTags(&obj, t) 125 | b.objects[path] = obj 126 | 127 | return backend.Metadata{ 128 | Tags: copyTags(obj.Tags), 129 | Version: obj.Version, 130 | }, nil 131 | } 132 | 133 | func (b *Backend) Write( 134 | ctx context.Context, 135 | path string, 136 | value []byte, 137 | t backend.Tags, 138 | ) (backend.Metadata, error) { 139 | if err := ctx.Err(); err != nil { 140 | return backend.Metadata{}, err 141 | } 142 | 143 | b.m.Lock() 144 | defer b.m.Unlock() 145 | 146 | obj := b.objects[path] 147 | b.updateTags(&obj, t) 148 | b.updateData(&obj, value) 149 | b.objects[path] = obj 150 | 151 | return backend.Metadata{ 152 | Tags: copyTags(obj.Tags), 153 | Version: obj.Version, 154 | }, nil 155 | } 156 | 157 | func (b *Backend) WriteIf( 158 | ctx context.Context, 159 | path string, 160 | value []byte, 161 | expected backend.Version, 162 | t backend.Tags, 163 | ) (backend.Metadata, error) { 164 | if err := ctx.Err(); err != nil { 165 | return backend.Metadata{}, err 166 | } 167 | 168 | b.m.Lock() 169 | defer b.m.Unlock() 170 | 171 | obj, ok := b.objects[path] 172 | if !ok { 173 | return backend.Metadata{}, backend.ErrNotFound 174 | } 175 | if obj.Version != expected { 176 | return backend.Metadata{}, backend.ErrPrecondition 177 | } 178 | b.updateTags(&obj, t) 179 | b.updateData(&obj, value) 180 | b.objects[path] = obj 181 | 182 | return backend.Metadata{ 183 | Tags: copyTags(obj.Tags), 184 | Version: obj.Version, 185 | }, nil 186 | } 187 | 188 | func (b *Backend) WriteIfNotExists( 189 | ctx context.Context, 190 | path string, 191 | value []byte, 192 | t backend.Tags, 193 | ) (backend.Metadata, error) { 194 | if err := ctx.Err(); err != nil { 195 | return backend.Metadata{}, err 196 | } 197 | 198 | b.m.Lock() 199 | defer b.m.Unlock() 200 | 201 | obj, ok := b.objects[path] 202 | if ok { 203 | return backend.Metadata{}, backend.ErrPrecondition 204 | } 205 | b.updateTags(&obj, t) 206 | b.updateData(&obj, value) 207 | b.objects[path] = obj 208 | 209 | return backend.Metadata{ 210 | Tags: copyTags(obj.Tags), 211 | Version: obj.Version, 212 | }, nil 213 | } 214 | 215 | func (b *Backend) Delete(ctx context.Context, path string) error { 216 | if err := ctx.Err(); err != nil { 217 | return err 218 | } 219 | 220 | b.m.Lock() 221 | defer b.m.Unlock() 222 | 223 | _, ok := b.objects[path] 224 | if !ok { 225 | return backend.ErrNotFound 226 | } 227 | delete(b.objects, path) 228 | return nil 229 | } 230 | 231 | func (b *Backend) DeleteIf( 232 | ctx context.Context, 233 | path string, 234 | expected backend.Version, 235 | ) error { 236 | if err := ctx.Err(); err != nil { 237 | return err 238 | } 239 | 240 | b.m.Lock() 241 | defer b.m.Unlock() 242 | 243 | obj, ok := b.objects[path] 244 | if !ok { 245 | return backend.ErrNotFound 246 | } 247 | if obj.Version != expected { 248 | return backend.ErrPrecondition 249 | } 250 | delete(b.objects, path) 251 | return nil 252 | } 253 | 254 | func (b *Backend) List(ctx context.Context, dirPath string) (backend.ListIter, error) { 255 | if err := ctx.Err(); err != nil { 256 | return nil, err 257 | } 258 | 259 | if !strings.HasSuffix(dirPath, "/") { 260 | dirPath += "/" 261 | } 262 | 263 | b.m.Lock() 264 | defer b.m.Unlock() 265 | 266 | ps := stringset.New() 267 | for k := range b.objects { 268 | if !strings.HasPrefix(k, dirPath) { 269 | continue 270 | } 271 | // Drop subdirs from the path. 272 | i := len(dirPath) 273 | for ; i < len(k) && k[i] != '/'; i++ { 274 | } 275 | ps.Add(k[:i]) 276 | } 277 | 278 | paths := ps.ToSlice() 279 | sort.Strings(paths) 280 | return &listIter{ 281 | paths: paths, 282 | }, nil 283 | } 284 | 285 | func (b *Backend) updateTags(obj *object, t backend.Tags) { 286 | if len(t) == 0 { 287 | return 288 | } 289 | if obj.Tags == nil { 290 | obj.Tags = make(backend.Tags) 291 | } 292 | for k, v := range t { 293 | obj.Tags[k] = v 294 | } 295 | obj.Version.Meta++ 296 | } 297 | 298 | func (b *Backend) updateData(obj *object, d []byte) { 299 | obj.Data = d 300 | obj.Version.Contents = b.nextGeneration() 301 | } 302 | 303 | func (b *Backend) nextGeneration() int64 { 304 | // Note that this is not locked. 305 | res := b.nextGen 306 | b.nextGen++ 307 | return res 308 | } 309 | 310 | type listIter struct { 311 | paths []string 312 | curr int 313 | } 314 | 315 | func (l *listIter) Next() (path string, ok bool) { 316 | if l.curr >= len(l.paths) { 317 | return "", false 318 | } 319 | res := l.paths[l.curr] 320 | l.curr++ 321 | return res, true 322 | } 323 | 324 | func (l *listIter) Err() error { 325 | return nil 326 | } 327 | 328 | type object struct { 329 | Data []byte 330 | Tags backend.Tags 331 | Version backend.Version 332 | } 333 | 334 | func copyTags(t backend.Tags) backend.Tags { 335 | if len(t) == 0 { 336 | return nil 337 | } 338 | res := make(backend.Tags) 339 | for k, v := range t { 340 | res[k] = v 341 | } 342 | return res 343 | } 344 | 345 | // Ensure that Backend interface is implemented correctly. 346 | var _ backend.Backend = (*Backend)(nil) 347 | -------------------------------------------------------------------------------- /backend/middleware/backendlogger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package middleware 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "log/slog" 21 | 22 | "github.com/mbrt/glassdb/backend" 23 | ) 24 | 25 | func NewBackendLogger(inner backend.Backend, id string, log *slog.Logger) BackendLogger { 26 | return BackendLogger{ 27 | inner: inner, 28 | log: log.With("backend-id", id), 29 | } 30 | } 31 | 32 | type BackendLogger struct { 33 | inner backend.Backend 34 | log *slog.Logger 35 | } 36 | 37 | func (b BackendLogger) ReadIfModified( 38 | ctx context.Context, 39 | path string, 40 | version int64, 41 | ) (backend.ReadReply, error) { 42 | r, err := b.inner.ReadIfModified(ctx, path, version) 43 | b.log.LogAttrs(ctx, slog.LevelDebug, "ReadIfModified", 44 | argsAttr("v:%d", version), pathAttr(path), resAttr(r), errAttr(err)) 45 | return r, err 46 | } 47 | 48 | func (b BackendLogger) Read(ctx context.Context, path string) (backend.ReadReply, error) { 49 | r, err := b.inner.Read(ctx, path) 50 | b.log.LogAttrs(ctx, slog.LevelDebug, "Read", pathAttr(path), resAttr(r), errAttr(err)) 51 | return r, err 52 | } 53 | 54 | func (b BackendLogger) GetMetadata( 55 | ctx context.Context, 56 | path string, 57 | ) (backend.Metadata, error) { 58 | r, err := b.inner.GetMetadata(ctx, path) 59 | b.log.LogAttrs(ctx, slog.LevelDebug, "GetMetadata", pathAttr(path), resFmtAttr("%+v", r), errAttr(err)) 60 | return r, err 61 | } 62 | 63 | func (b BackendLogger) SetTagsIf( 64 | ctx context.Context, 65 | path string, 66 | expected backend.Version, 67 | t backend.Tags, 68 | ) (backend.Metadata, error) { 69 | r, err := b.inner.SetTagsIf(ctx, path, expected, t) 70 | b.log.LogAttrs(ctx, slog.LevelDebug, "SetTagsIf", pathAttr(path), 71 | argsAttr("expv:%+v;t:%v", expected, t), resFmtAttr("%+v", r), errAttr(err)) 72 | return r, err 73 | } 74 | 75 | func (b BackendLogger) Write( 76 | ctx context.Context, 77 | path string, 78 | value []byte, 79 | t backend.Tags, 80 | ) (backend.Metadata, error) { 81 | r, err := b.inner.Write(ctx, path, value, t) 82 | b.log.LogAttrs(ctx, slog.LevelDebug, "Write", pathAttr(path), 83 | argsAttr("val[size]:%d;t:%v", len(value), t), resFmtAttr("%+v", r), errAttr(err)) 84 | return r, err 85 | } 86 | 87 | func (b BackendLogger) WriteIf( 88 | ctx context.Context, 89 | path string, 90 | value []byte, 91 | expected backend.Version, 92 | t backend.Tags, 93 | ) (backend.Metadata, error) { 94 | r, err := b.inner.WriteIf(ctx, path, value, expected, t) 95 | b.log.LogAttrs(ctx, slog.LevelDebug, "WriteIf", pathAttr(path), 96 | argsAttr("val[size]:%d;expv=%+v;t:%v", len(value), expected, t), 97 | resFmtAttr("%+v", r), errAttr(err)) 98 | return r, err 99 | } 100 | 101 | func (b BackendLogger) WriteIfNotExists( 102 | ctx context.Context, 103 | path string, 104 | value []byte, 105 | t backend.Tags, 106 | ) (backend.Metadata, error) { 107 | r, err := b.inner.WriteIfNotExists(ctx, path, value, t) 108 | b.log.LogAttrs(ctx, slog.LevelDebug, "WriteIfNotExists", pathAttr(path), 109 | argsAttr("val[size]:%d;t:%v", len(value), t), 110 | resFmtAttr("%+v", r), errAttr(err)) 111 | return r, err 112 | } 113 | 114 | func (b BackendLogger) Delete(ctx context.Context, path string) error { 115 | err := b.inner.Delete(ctx, path) 116 | b.log.LogAttrs(ctx, slog.LevelDebug, "Delete", pathAttr(path), errAttr(err)) 117 | return err 118 | } 119 | 120 | func (b BackendLogger) DeleteIf( 121 | ctx context.Context, 122 | path string, 123 | expected backend.Version, 124 | ) error { 125 | err := b.inner.DeleteIf(ctx, path, expected) 126 | b.log.LogAttrs(ctx, slog.LevelDebug, "DeleteIf", pathAttr(path), 127 | argsAttr("expv:%+v", expected), errAttr(err)) 128 | return err 129 | } 130 | 131 | func (b BackendLogger) List(ctx context.Context, dirPath string) (backend.ListIter, error) { 132 | r, err := b.inner.List(ctx, dirPath) 133 | b.log.LogAttrs(ctx, slog.LevelDebug, "List", pathAttr(dirPath), errAttr(err)) 134 | return r, err 135 | } 136 | 137 | type readReplyLog backend.ReadReply 138 | 139 | func (r readReplyLog) LogValue() slog.Value { 140 | return slog.StringValue(fmt.Sprintf("{cont[size]=%d, v=%+v}", len(r.Contents), r.Version)) 141 | } 142 | 143 | func resAttr(r backend.ReadReply) slog.Attr { 144 | return slog.Attr{ 145 | Key: "res", 146 | Value: slog.AnyValue(readReplyLog(r)), 147 | } 148 | } 149 | 150 | func resFmtAttr(format string, v ...any) slog.Attr { 151 | return slog.Attr{ 152 | Key: "res", 153 | Value: slog.StringValue(fmt.Sprintf(format, v...)), 154 | } 155 | } 156 | 157 | func errAttr(err error) slog.Attr { 158 | if err == nil { 159 | return slog.Attr{} 160 | } 161 | return slog.Attr{ 162 | Key: "err", 163 | Value: slog.StringValue(err.Error()), 164 | } 165 | } 166 | 167 | func pathAttr(p string) slog.Attr { 168 | return slog.Attr{ 169 | Key: "path", 170 | Value: slog.StringValue(p), 171 | } 172 | } 173 | 174 | func argsAttr(format string, v ...any) slog.Attr { 175 | return slog.Attr{ 176 | Key: "args", 177 | Value: slog.StringValue(fmt.Sprintf(format, v...)), 178 | } 179 | } 180 | 181 | // Ensure that Backend interface is implemented correctly. 182 | var _ backend.Backend = (*BackendLogger)(nil) 183 | -------------------------------------------------------------------------------- /backend/middleware/delaybackend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package middleware 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "math" 21 | "math/rand" 22 | "sync" 23 | "time" 24 | 25 | "github.com/jonboulle/clockwork" 26 | 27 | "github.com/mbrt/glassdb/backend" 28 | "github.com/mbrt/glassdb/internal/concurr" 29 | ) 30 | 31 | var errBackoff = errors.New("rate limited") 32 | 33 | var GCSDelays = DelayOptions{ 34 | MetaRead: Latency{22 * time.Millisecond, 7 * time.Millisecond}, 35 | MetaWrite: Latency{31 * time.Millisecond, 8 * time.Millisecond}, 36 | ObjRead: Latency{57 * time.Millisecond, 7 * time.Millisecond}, 37 | ObjWrite: Latency{70 * time.Millisecond, 15 * time.Millisecond}, 38 | List: Latency{10 * time.Millisecond, 3 * time.Millisecond}, 39 | SameObjWritePs: 1, 40 | } 41 | 42 | type DelayOptions struct { 43 | MetaRead Latency 44 | MetaWrite Latency 45 | ObjRead Latency 46 | ObjWrite Latency 47 | List Latency 48 | // How many writes per second to the same object before being 49 | // rate limited? 50 | SameObjWritePs int 51 | } 52 | 53 | type Latency struct { 54 | Mean time.Duration 55 | StdDev time.Duration 56 | } 57 | 58 | func NewDelayBackend( 59 | inner backend.Backend, 60 | clock clockwork.Clock, 61 | opts DelayOptions, 62 | ) *DelayBackend { 63 | return &DelayBackend{ 64 | inner: inner, 65 | clock: clock, 66 | metaRead: lognormalDelay(opts.MetaRead), 67 | metaWrite: lognormalDelay(opts.MetaWrite), 68 | objRead: lognormalDelay(opts.ObjRead), 69 | objWrite: lognormalDelay(opts.ObjWrite), 70 | list: lognormalDelay(opts.List), 71 | rlimit: rateLimiter{ 72 | tokensPerSec: opts.SameObjWritePs, 73 | clock: clock, 74 | buckets: map[string]bucketState{}, 75 | }, 76 | retryDelay: opts.ObjWrite.Mean * 2, 77 | } 78 | } 79 | 80 | type DelayBackend struct { 81 | inner backend.Backend 82 | clock clockwork.Clock 83 | metaRead lognormal 84 | metaWrite lognormal 85 | objRead lognormal 86 | objWrite lognormal 87 | list lognormal 88 | rlimit rateLimiter 89 | retryDelay time.Duration 90 | } 91 | 92 | func (b *DelayBackend) ReadIfModified( 93 | ctx context.Context, 94 | path string, 95 | version int64, 96 | ) (backend.ReadReply, error) { 97 | b.delay(b.objRead) 98 | return b.inner.ReadIfModified(ctx, path, version) 99 | } 100 | 101 | func (b *DelayBackend) Read(ctx context.Context, path string) (backend.ReadReply, error) { 102 | b.delay(b.objRead) 103 | r, err := b.inner.Read(ctx, path) 104 | return r, err 105 | } 106 | 107 | func (b *DelayBackend) GetMetadata( 108 | ctx context.Context, 109 | path string, 110 | ) (backend.Metadata, error) { 111 | b.delay(b.metaRead) 112 | r, err := b.inner.GetMetadata(ctx, path) 113 | return r, err 114 | } 115 | 116 | func (b *DelayBackend) SetTagsIf( 117 | ctx context.Context, 118 | path string, 119 | expected backend.Version, 120 | t backend.Tags, 121 | ) (backend.Metadata, error) { 122 | if err := b.backoff(ctx, path); err != nil { 123 | return backend.Metadata{}, err 124 | } 125 | b.delay(b.metaWrite) 126 | return b.inner.SetTagsIf(ctx, path, expected, t) 127 | } 128 | 129 | func (b *DelayBackend) Write( 130 | ctx context.Context, 131 | path string, 132 | value []byte, 133 | t backend.Tags, 134 | ) (backend.Metadata, error) { 135 | if err := b.backoff(ctx, path); err != nil { 136 | return backend.Metadata{}, err 137 | } 138 | b.delay(b.objWrite) 139 | r, err := b.inner.Write(ctx, path, value, t) 140 | return r, err 141 | } 142 | 143 | func (b *DelayBackend) WriteIf( 144 | ctx context.Context, 145 | path string, 146 | value []byte, 147 | expected backend.Version, 148 | t backend.Tags, 149 | ) (backend.Metadata, error) { 150 | if err := b.backoff(ctx, path); err != nil { 151 | return backend.Metadata{}, err 152 | } 153 | b.delay(b.objWrite) 154 | return b.inner.WriteIf(ctx, path, value, expected, t) 155 | } 156 | 157 | func (b *DelayBackend) WriteIfNotExists( 158 | ctx context.Context, 159 | path string, 160 | value []byte, 161 | t backend.Tags, 162 | ) (backend.Metadata, error) { 163 | if err := b.backoff(ctx, path); err != nil { 164 | return backend.Metadata{}, err 165 | } 166 | b.delay(b.objWrite) 167 | return b.inner.WriteIfNotExists(ctx, path, value, t) 168 | } 169 | 170 | func (b *DelayBackend) Delete(ctx context.Context, path string) error { 171 | if err := b.backoff(ctx, path); err != nil { 172 | return err 173 | } 174 | b.delay(b.objWrite) 175 | err := b.inner.Delete(ctx, path) 176 | return err 177 | } 178 | 179 | func (b *DelayBackend) DeleteIf( 180 | ctx context.Context, 181 | path string, 182 | expected backend.Version, 183 | ) error { 184 | if err := b.backoff(ctx, path); err != nil { 185 | return err 186 | } 187 | b.delay(b.objWrite) 188 | return b.inner.DeleteIf(ctx, path, expected) 189 | } 190 | 191 | func (b *DelayBackend) List(ctx context.Context, dirPath string) (backend.ListIter, error) { 192 | b.delay(b.list) 193 | r, err := b.inner.List(ctx, dirPath) 194 | return r, err 195 | } 196 | 197 | func (b *DelayBackend) backoff(ctx context.Context, path string) error { 198 | r := concurr.RetryOptions(b.retryDelay, b.retryDelay*10, b.clock) 199 | return r.Retry(ctx, func() error { 200 | if !b.rlimit.TryAcquireToken(path) { 201 | return errBackoff 202 | } 203 | return nil 204 | }) 205 | } 206 | 207 | func (b *DelayBackend) delay(ln lognormal) { 208 | ms := ln.Rand() 209 | d := time.Duration(ms * float64(time.Millisecond)) 210 | b.clock.Sleep(d) 211 | } 212 | 213 | func lognormalDelay(l Latency) lognormal { 214 | m64 := float64(l.Mean) / float64(time.Millisecond) 215 | s64 := float64(l.StdDev) / float64(time.Millisecond) 216 | return newLognormalWith(m64, s64) 217 | } 218 | 219 | func newLognormalWith(mean, stddev float64) lognormal { 220 | // https://stats.stackexchange.com/a/95506 221 | sByM := stddev / mean 222 | v := math.Log(sByM*sByM + 1) 223 | return lognormal{ 224 | Mu: math.Log(mean) - 0.5*v, 225 | Sigma: math.Sqrt(v), 226 | } 227 | } 228 | 229 | type lognormal struct { 230 | Mu float64 231 | Sigma float64 232 | } 233 | 234 | func (l lognormal) Rand() float64 { 235 | return math.Exp(rand.NormFloat64()*l.Sigma + l.Mu) 236 | } 237 | 238 | type rateLimiter struct { 239 | tokensPerSec int 240 | clock clockwork.Clock 241 | buckets map[string]bucketState 242 | m sync.Mutex 243 | } 244 | 245 | func (r *rateLimiter) TryAcquireToken(key string) bool { 246 | if r.tokensPerSec == 0 { 247 | return false 248 | } 249 | 250 | r.m.Lock() 251 | defer r.m.Unlock() 252 | 253 | now := r.clock.Now() 254 | entry, ok := r.buckets[key] 255 | if !ok { 256 | r.buckets[key] = bucketState{ 257 | LastCheck: now, 258 | Tokens: r.tokensPerSec - 1, 259 | } 260 | return true 261 | } 262 | 263 | elapsed := now.Sub(entry.LastCheck) 264 | if elapsed >= time.Second { 265 | newTokens := entry.Tokens + int(elapsed.Seconds()*float64(r.tokensPerSec)) 266 | if newTokens > r.tokensPerSec { 267 | newTokens = r.tokensPerSec 268 | } 269 | if newTokens <= 0 { 270 | return false 271 | } 272 | r.buckets[key] = bucketState{ 273 | LastCheck: now, 274 | Tokens: newTokens - 1, 275 | } 276 | return true 277 | } 278 | 279 | entry.Tokens-- 280 | r.buckets[key] = entry 281 | return true 282 | } 283 | 284 | type bucketState struct { 285 | LastCheck time.Time 286 | Tokens int 287 | } 288 | 289 | // Ensure that Backend interface is implemented correctly. 290 | var _ backend.Backend = (*DelayBackend)(nil) 291 | -------------------------------------------------------------------------------- /backend/middleware/delaybackend_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package middleware 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/jonboulle/clockwork" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestRateLimiter(t *testing.T) { 26 | clock := clockwork.NewFakeClock() 27 | rt := rateLimiter{ 28 | tokensPerSec: 1, 29 | clock: clock, 30 | buckets: make(map[string]bucketState), 31 | } 32 | 33 | begin := clock.Now() 34 | assert.True(t, rt.TryAcquireToken("k")) 35 | clock.Advance(100 * time.Millisecond) 36 | assert.True(t, rt.TryAcquireToken("k")) 37 | clock.Advance(100 * time.Millisecond) 38 | assert.True(t, rt.TryAcquireToken("k")) 39 | clock.Advance(700 * time.Millisecond) 40 | assert.True(t, rt.TryAcquireToken("k")) 41 | clock.Advance(150 * time.Millisecond) 42 | // Here 1050ms passed. 43 | // We were able to sneak in 3 extra requests, so we should be rejected for 44 | // around 4 seconds. 45 | for clock.Now().Sub(begin) < 4*time.Second { 46 | assert.False(t, rt.TryAcquireToken("k"), "elapsed: %v", clock.Now().Sub(begin)) 47 | clock.Advance(250 * time.Millisecond) 48 | } 49 | // We can sneak in more requests. 50 | for i := 0; i < 5; i++ { 51 | assert.True(t, rt.TryAcquireToken("k"), "i: %d", i) 52 | } 53 | clock.Advance(time.Second) 54 | begin = clock.Now() 55 | // And now we should be blocked again for 5 seconds. 56 | for clock.Now().Sub(begin) < 4*time.Second { 57 | assert.False(t, rt.TryAcquireToken("k"), "elapsed: %v", clock.Now().Sub(begin)) 58 | clock.Advance(250 * time.Millisecond) 59 | } 60 | assert.True(t, rt.TryAcquireToken("k")) 61 | } 62 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto # Uses the current coverage as a baseline. 6 | threshold: 1% # Allows up to a 1% decrease without failing. 7 | patch: 8 | default: 9 | target: 80% # Desired coverage for new code changes. 10 | threshold: 1% # Allows up to a 1% decrease in patch coverage. 11 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package glassdb 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "time" 21 | 22 | "github.com/mbrt/glassdb/backend" 23 | "github.com/mbrt/glassdb/internal/data/paths" 24 | "github.com/mbrt/glassdb/internal/storage" 25 | "github.com/mbrt/glassdb/internal/trans" 26 | ) 27 | 28 | var collInfoContents = []byte("__collection__") 29 | 30 | type Collection struct { 31 | prefix string 32 | global storage.Global 33 | local storage.Local 34 | algo trans.Algo 35 | db *DB 36 | } 37 | 38 | func (c Collection) ReadStrong(ctx context.Context, key []byte) ([]byte, error) { 39 | var ( 40 | res []byte 41 | readErr error 42 | ) 43 | 44 | err := c.db.Tx(ctx, func(tx *Tx) error { 45 | res, readErr = tx.Read(c, key) 46 | if !errors.Is(readErr, backend.ErrNotFound) { 47 | // We need to validate the transaction even when we don't find the 48 | // value. It's not enough to abort. 49 | return readErr 50 | } 51 | return nil 52 | }) 53 | 54 | // If the transaction failed, return that error. 55 | // Otherwise, return the read error. 56 | if err != nil { 57 | return res, err 58 | } 59 | return res, readErr 60 | } 61 | 62 | func (c Collection) ReadWeak( 63 | ctx context.Context, 64 | key []byte, 65 | maxStaleness time.Duration, 66 | ) ([]byte, error) { 67 | p := paths.FromKey(c.prefix, key) 68 | r := trans.NewReader(c.local, c.global, c.db.tmon) 69 | rv, err := r.Read(ctx, p, maxStaleness) 70 | return rv.Value, err 71 | } 72 | 73 | func (c Collection) Write(ctx context.Context, key, value []byte) error { 74 | return c.db.Tx(ctx, func(tx *Tx) error { 75 | return tx.Write(c, key, value) 76 | }) 77 | } 78 | 79 | func (c Collection) Delete(ctx context.Context, key []byte) error { 80 | return c.db.Tx(ctx, func(tx *Tx) error { 81 | return tx.Delete(c, key) 82 | }) 83 | } 84 | 85 | func (c Collection) Update( 86 | ctx context.Context, 87 | key []byte, 88 | f func(old []byte) ([]byte, error), 89 | ) ([]byte, error) { 90 | var newb []byte 91 | 92 | err := c.db.Tx(ctx, func(tx *Tx) error { 93 | old, err := tx.Read(c, key) 94 | if err != nil { 95 | return err 96 | } 97 | newb, err = f(old) 98 | if err != nil { 99 | return err 100 | } 101 | return tx.Write(c, key, newb) 102 | }) 103 | 104 | return newb, err 105 | } 106 | 107 | func (c Collection) Collection(name []byte) Collection { 108 | p := paths.FromCollection(c.prefix, name) 109 | return c.db.openCollection(p) 110 | } 111 | 112 | func (c Collection) Create(ctx context.Context) error { 113 | p := paths.CollectionInfo(c.prefix) 114 | _, err := c.global.GetMetadata(ctx, p) 115 | if err == nil { 116 | // All good, the collection is there. 117 | return nil 118 | } 119 | if !errors.Is(err, backend.ErrNotFound) { 120 | // Some other error. Bail out. 121 | return err 122 | } 123 | // The collection is not there; create it. 124 | _, err = c.global.WriteIfNotExists(ctx, p, collInfoContents, nil) 125 | if errors.Is(err, backend.ErrPrecondition) { 126 | // This was created concurrently. Assume it's there. 127 | return nil 128 | } 129 | return err 130 | } 131 | 132 | func (c Collection) Keys(ctx context.Context) (*KeysIter, error) { 133 | keysPrefix := paths.KeysPrefix(c.prefix) 134 | iter, err := c.db.backend.List(ctx, keysPrefix) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return &KeysIter{inner: iter}, nil 139 | } 140 | 141 | func (c Collection) Collections(ctx context.Context) (*CollectionsIter, error) { 142 | cprefix := paths.CollectionsPrefix(c.prefix) 143 | iter, err := c.db.backend.List(ctx, cprefix) 144 | if err != nil { 145 | return nil, err 146 | } 147 | return &CollectionsIter{inner: iter}, nil 148 | } 149 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package glassdb 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "log/slog" 22 | "regexp" 23 | "sync" 24 | 25 | "github.com/jonboulle/clockwork" 26 | 27 | "github.com/mbrt/glassdb/backend" 28 | "github.com/mbrt/glassdb/internal/cache" 29 | "github.com/mbrt/glassdb/internal/concurr" 30 | "github.com/mbrt/glassdb/internal/data/paths" 31 | "github.com/mbrt/glassdb/internal/storage" 32 | "github.com/mbrt/glassdb/internal/trace" 33 | "github.com/mbrt/glassdb/internal/trans" 34 | ) 35 | 36 | var nameRegexp = regexp.MustCompile(`^[a-zA-Z0-9]+$`) 37 | 38 | // DefaultOptions provides the options used by `Open`. They should be a good 39 | // middle ground for a production deployment. 40 | func DefaultOptions() Options { 41 | return Options{ 42 | Clock: clockwork.NewRealClock(), 43 | Logger: slog.Default(), 44 | CacheSize: 5012 * 1024 * 1024, // 512 MiB 45 | } 46 | } 47 | 48 | // Options makes it possible to tweak a client DB. 49 | // 50 | // TODO: Add retry timing options. 51 | type Options struct { 52 | Clock clockwork.Clock 53 | Logger *slog.Logger 54 | // Cache size is the number of bytes dedicated to caching objects and 55 | // and metadata. Setting this too small may impact performance, as 56 | // more calls to the backend would be necessary. 57 | CacheSize int 58 | } 59 | 60 | func Open(ctx context.Context, name string, b backend.Backend) (*DB, error) { 61 | return OpenWith(ctx, name, b, DefaultOptions()) 62 | } 63 | 64 | func OpenWith(ctx context.Context, name string, b backend.Backend, opts Options) (*DB, error) { 65 | if !nameRegexp.MatchString(name) { 66 | return nil, fmt.Errorf("name must be alphanumeric, got %q", name) 67 | } 68 | if err := checkOrCreateDBMeta(ctx, b, name); err != nil { 69 | return nil, err 70 | } 71 | 72 | cache := cache.New(opts.CacheSize) 73 | bg := concurr.NewBackground() 74 | 75 | backend := &statsBackend{inner: b} 76 | local := storage.NewLocal(cache, opts.Clock) 77 | global := storage.NewGlobal(backend, local, opts.Clock) 78 | tl := storage.NewTLogger(opts.Clock, global, local, name) 79 | tmon := trans.NewMonitor(opts.Clock, local, tl, bg) 80 | locker := trans.NewLocker(local, global, tl, opts.Clock, tmon) 81 | gc := trans.NewGC(opts.Clock, bg, tl, opts.Logger) 82 | gc.Start(ctx) 83 | ta := trans.NewAlgo( 84 | opts.Clock, 85 | global, 86 | local, 87 | locker, 88 | tmon, 89 | gc, 90 | bg, 91 | opts.Logger, 92 | ) 93 | 94 | res := &DB{ 95 | name: name, 96 | backend: backend, 97 | cache: cache, 98 | background: bg, 99 | tmon: tmon, 100 | gc: gc, 101 | algo: ta, 102 | clock: opts.Clock, 103 | logger: opts.Logger, 104 | } 105 | res.root = res.openCollection(name) 106 | 107 | return res, nil 108 | } 109 | 110 | type DB struct { 111 | name string 112 | backend *statsBackend 113 | cache *cache.Cache 114 | background *concurr.Background 115 | tmon *trans.Monitor 116 | gc *trans.GC 117 | algo trans.Algo 118 | clock clockwork.Clock 119 | logger *slog.Logger 120 | root Collection 121 | stats Stats 122 | statsM sync.Mutex 123 | } 124 | 125 | func (d *DB) Close(context.Context) error { 126 | d.background.Close() 127 | return nil 128 | } 129 | 130 | func (d *DB) Collection(name []byte) Collection { 131 | p := paths.FromCollection(d.root.prefix, name) 132 | return d.openCollection(p) 133 | } 134 | 135 | func (d *DB) Tx(ctx context.Context, f func(tx *Tx) error) error { 136 | stats := &Stats{TxN: 1} 137 | begin := d.clock.Now() 138 | ctx, task := trace.NewTask(ctx, "tx") 139 | err := d.txImpl(ctx, f, stats) 140 | task.End() 141 | stats.TxTime = d.clock.Now().Sub(begin) 142 | d.updateStats(stats) 143 | return err 144 | } 145 | 146 | // Stats retrieves ongoing performance stats for the database. This is only 147 | // updated when a transaction closes. 148 | // 149 | // Counters only increase over time and are never reset. If you need to measure 150 | // a specific interval only, please see Stats.Sub. 151 | func (d *DB) Stats() Stats { 152 | d.statsM.Lock() 153 | defer d.statsM.Unlock() 154 | 155 | // Update backend stats now. 156 | bstats := d.backend.StatsAndReset() 157 | d.stats.add(&bstats) 158 | return d.stats 159 | } 160 | 161 | func (d *DB) openCollection(prefix string) Collection { 162 | local := storage.NewLocal(d.cache, d.clock) 163 | global := storage.NewGlobal(d.backend, local, d.clock) 164 | return Collection{prefix, global, local, d.algo, d} 165 | } 166 | 167 | func (d *DB) txImpl(ctx context.Context, fn func(tx *Tx) error, stats *Stats) (err error) { 168 | local := storage.NewLocal(d.cache, d.clock) 169 | global := storage.NewGlobal(d.backend, local, d.clock) 170 | 171 | tx := newTx(ctx, global, local, d.tmon) 172 | var handle *trans.Handle 173 | defer func() { 174 | // Wrapping this in func() is important, because handle is modified 175 | // after the defer statement is declared. We need to call End with 176 | // the updated parameter. 177 | if e := d.algo.End(ctx, handle); e != nil && err == nil { 178 | err = e 179 | } 180 | }() 181 | 182 | for { 183 | if err := ctx.Err(); err != nil { 184 | return err 185 | } 186 | var fnErr error 187 | trace.WithRegion(ctx, "user-tx", func() { 188 | fnErr = fn(tx) 189 | }) 190 | if tx.aborted { 191 | return ErrAborted 192 | } 193 | 194 | // Collect the access. 195 | access := tx.collectAccesses() 196 | stats.TxReads += len(access.Reads) 197 | stats.TxWrites += len(access.Writes) 198 | if handle == nil { 199 | handle = d.algo.Begin(ctx, access) 200 | } else { 201 | d.algo.Reset(handle, access) 202 | } 203 | 204 | if fnErr != nil { 205 | // The user function returned an error. We need to check whether 206 | // this could be a result of a spurious read instead of a true 207 | // failure. 208 | // To do this, we validate only the reads. 209 | access.Writes = nil 210 | d.algo.Reset(handle, access) 211 | err := d.algo.ValidateReads(ctx, handle) 212 | if errors.Is(err, trans.ErrRetry) { 213 | // The failure may be spurious. Retry the transaction. 214 | tx.reset() 215 | stats.TxRetries++ 216 | continue 217 | } 218 | // Otherwise we need to return the error coming from 'fn'. 219 | return fnErr 220 | } 221 | 222 | // Try to commit. 223 | trace.WithRegion(ctx, "commit", func() { 224 | err = d.algo.Commit(ctx, handle) 225 | }) 226 | if err != nil { 227 | if errors.Is(err, trans.ErrRetry) { 228 | // Retry the transaction. 229 | tx.reset() 230 | stats.TxRetries++ 231 | continue 232 | } 233 | // Consider aborted. 234 | return err 235 | } 236 | 237 | // We are committed. 238 | break 239 | } 240 | 241 | return nil 242 | } 243 | 244 | func (d *DB) updateStats(s *Stats) { 245 | d.statsM.Lock() 246 | d.stats.add(s) 247 | d.statsM.Unlock() 248 | } 249 | -------------------------------------------------------------------------------- /demo/demo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "log" 22 | "os" 23 | 24 | "cloud.google.com/go/storage" 25 | 26 | "github.com/mbrt/glassdb" 27 | "github.com/mbrt/glassdb/backend" 28 | "github.com/mbrt/glassdb/backend/gcs" 29 | ) 30 | 31 | func env(k string) (string, error) { 32 | if v := os.Getenv(k); v != "" { 33 | return v, nil 34 | } 35 | return "", fmt.Errorf("environment variable $%v is required", k) 36 | } 37 | 38 | func initStorage(ctx context.Context) (*storage.Client, error) { 39 | client, err := storage.NewClient(ctx) 40 | if err != nil { 41 | return nil, fmt.Errorf("creating client: %w", err) 42 | } 43 | return client, nil 44 | } 45 | 46 | func initBackend(client *storage.Client) (backend.Backend, error) { 47 | bucket, err := env("BUCKET") 48 | if err != nil { 49 | return nil, err 50 | } 51 | return gcs.New(client.Bucket(bucket)), nil 52 | } 53 | 54 | func do() error { 55 | ctx := context.Background() 56 | client, err := initStorage(ctx) 57 | if err != nil { 58 | return err 59 | } 60 | b, err := initBackend(client) 61 | if err != nil { 62 | return err 63 | } 64 | db, err := glassdb.Open(ctx, "example", b) 65 | if err != nil { 66 | return fmt.Errorf("opening db: %w", err) 67 | } 68 | defer db.Close(ctx) 69 | 70 | key := []byte("key1") 71 | val := []byte("value1") 72 | 73 | coll := db.Collection([]byte("demo-coll")) 74 | if err := coll.Create(ctx); err != nil { 75 | return err 76 | } 77 | if err := coll.Write(ctx, key, val); err != nil { 78 | return err 79 | } 80 | buf, err := coll.ReadStrong(ctx, key) 81 | if err != nil { 82 | return err 83 | } 84 | if !bytes.Equal(buf, val) { 85 | return fmt.Errorf("read(%q) = %q, expected %q", string(key), string(buf), string(val)) 86 | } 87 | return nil 88 | } 89 | 90 | func main() { 91 | if err := do(); err != nil { 92 | log.Printf("Error: %v", err) 93 | os.Exit(1) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docs/img/deadlock-latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrt/glassdb/3b42bc1c150c75296cc3f712eabda75b3807c778/docs/img/deadlock-latency.png -------------------------------------------------------------------------------- /docs/img/ops-latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrt/glassdb/3b42bc1c150c75296cc3f712eabda75b3807c778/docs/img/ops-latency.png -------------------------------------------------------------------------------- /docs/img/retries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrt/glassdb/3b42bc1c150c75296cc3f712eabda75b3807c778/docs/img/retries.png -------------------------------------------------------------------------------- /docs/img/tx-latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrt/glassdb/3b42bc1c150c75296cc3f712eabda75b3807c778/docs/img/tx-latency.png -------------------------------------------------------------------------------- /docs/img/tx-throughput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbrt/glassdb/3b42bc1c150c75296cc3f712eabda75b3807c778/docs/img/tx-throughput.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mbrt/glassdb 2 | 3 | go 1.23 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.49.0 7 | github.com/cenkalti/backoff/v4 v4.3.0 8 | github.com/golang/protobuf v1.5.4 9 | github.com/googleapis/gax-go/v2 v2.14.1 10 | github.com/gorilla/mux v1.8.1 11 | github.com/jonboulle/clockwork v0.5.0 12 | github.com/sourcegraph/conc v0.3.0 13 | github.com/stretchr/testify v1.10.0 14 | golang.org/x/sync v0.10.0 15 | google.golang.org/api v0.214.0 16 | google.golang.org/protobuf v1.36.1 17 | ) 18 | 19 | require ( 20 | cel.dev/expr v0.16.1 // indirect 21 | cloud.google.com/go v0.116.0 // indirect 22 | cloud.google.com/go/auth v0.13.0 // indirect 23 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect 24 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 25 | cloud.google.com/go/iam v1.2.2 // indirect 26 | cloud.google.com/go/monitoring v1.21.2 // indirect 27 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect 28 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect 30 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/envoyproxy/go-control-plane v0.13.1 // indirect 35 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 36 | github.com/felixge/httpsnoop v1.0.4 // indirect 37 | github.com/go-logr/logr v1.4.2 // indirect 38 | github.com/go-logr/stdr v1.2.2 // indirect 39 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 40 | github.com/google/s2a-go v0.1.8 // indirect 41 | github.com/google/uuid v1.6.0 // indirect 42 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 43 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 44 | github.com/pmezard/go-difflib v1.0.0 // indirect 45 | go.opencensus.io v0.24.0 // indirect 46 | go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect 47 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 49 | go.opentelemetry.io/otel v1.29.0 // indirect 50 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 51 | go.opentelemetry.io/otel/sdk v1.29.0 // indirect 52 | go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect 53 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 54 | golang.org/x/crypto v0.31.0 // indirect 55 | golang.org/x/net v0.33.0 // indirect 56 | golang.org/x/oauth2 v0.24.0 // indirect 57 | golang.org/x/sys v0.28.0 // indirect 58 | golang.org/x/text v0.21.0 // indirect 59 | golang.org/x/time v0.8.0 // indirect 60 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect 61 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 62 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 63 | google.golang.org/grpc v1.67.3 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /hack/backendbench/backendbench.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "crypto/rand" 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "path" 25 | "time" 26 | 27 | "cloud.google.com/go/storage" 28 | "github.com/jonboulle/clockwork" 29 | 30 | "github.com/mbrt/glassdb/backend" 31 | "github.com/mbrt/glassdb/backend/gcs" 32 | "github.com/mbrt/glassdb/backend/memory" 33 | "github.com/mbrt/glassdb/backend/middleware" 34 | "github.com/mbrt/glassdb/internal/testkit/bench" 35 | ) 36 | 37 | const ( 38 | testRoot = "backend-bench" 39 | testDuration = 20 * time.Second 40 | ) 41 | 42 | var backendType = flag.String("backend", "memory", "select backend type [memory|gcs]") 43 | 44 | func initBackend() (backend.Backend, error) { 45 | ctx := context.Background() 46 | 47 | switch *backendType { 48 | case "memory": 49 | backend := memory.New() 50 | clock := clockwork.NewRealClock() 51 | return middleware.NewDelayBackend(backend, clock, middleware.GCSDelays), nil 52 | case "gcs": 53 | return initGCSBackend(ctx) 54 | } 55 | 56 | return nil, fmt.Errorf("unknown backend type %q", *backendType) 57 | } 58 | 59 | func initGCSBackend(ctx context.Context) (backend.Backend, error) { 60 | client, err := storage.NewClient(ctx) 61 | if err != nil { 62 | return nil, fmt.Errorf("creating client: %w", err) 63 | } 64 | bucket, err := env("BUCKET") 65 | if err != nil { 66 | return nil, err 67 | } 68 | return gcs.New(client.Bucket(bucket)), nil 69 | } 70 | 71 | func env(k string) (string, error) { 72 | if v := os.Getenv(k); v != "" { 73 | return v, nil 74 | } 75 | return "", fmt.Errorf("environment variable $%v is required", k) 76 | } 77 | 78 | func runBench(name string, b backend.Backend, fn func(backend.Backend, *bench.Bench) error) error { 79 | var ben bench.Bench 80 | ben.SetDuration(testDuration) 81 | ben.Start() 82 | if err := fn(b, &ben); err != nil { 83 | return err 84 | } 85 | ben.End() 86 | res := ben.Results() 87 | 88 | var ts time.Duration 89 | for _, x := range res.Samples { 90 | fmt.Printf("%s,%s,%s\n", name, formatMs(ts), formatMs(x)) 91 | ts += x 92 | } 93 | 94 | log.Printf("%s: 50pc: %v, 90pc: %v, 95pc: %v", name, 95 | res.Percentile(0.5), res.Percentile(0.9), res.Percentile(0.95)) 96 | 97 | return nil 98 | } 99 | 100 | func benchWriteSame(b backend.Backend, ben *bench.Bench) error { 101 | ctx := context.Background() 102 | data := randomData(1024) 103 | p := path.Join(testRoot, "write-same") 104 | count := 0 105 | 106 | for !ben.IsTestFinished() { 107 | err := ben.Measure(func() error { 108 | _, err := b.Write(ctx, p, data, backend.Tags{ 109 | "key": fmt.Sprintf("val%d", count), 110 | }) 111 | return err 112 | }) 113 | if err != nil { 114 | return err 115 | } 116 | count++ 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func benchWriteFailPre(b backend.Backend, ben *bench.Bench) error { 123 | ctx := context.Background() 124 | data := randomData(1024) 125 | p := path.Join(testRoot, "write-same") 126 | meta, err := b.Write(ctx, p, data, backend.Tags{"key": "val"}) 127 | if err != nil { 128 | return err 129 | } 130 | count := 0 131 | 132 | expected := meta.Version 133 | expected.Meta++ 134 | 135 | for !ben.IsTestFinished() { 136 | err := ben.Measure(func() error { 137 | _, _ = b.WriteIf(ctx, p, data, expected, backend.Tags{ 138 | "key": fmt.Sprintf("val%d", count), 139 | }) 140 | return nil 141 | }) 142 | if err != nil { 143 | return err 144 | } 145 | count++ 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func benchRead(b backend.Backend, ben *bench.Bench) error { 152 | ctx := context.Background() 153 | data := randomData(1024) 154 | 155 | p := path.Join(testRoot, "read") 156 | _, err := b.Write(ctx, p, data, backend.Tags{"key": "val"}) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | for !ben.IsTestFinished() { 162 | err := ben.Measure(func() error { 163 | _, err := b.Read(ctx, p) 164 | return err 165 | }) 166 | if err != nil { 167 | return err 168 | } 169 | } 170 | 171 | return nil 172 | } 173 | 174 | func benchReadUnchanged(b backend.Backend, ben *bench.Bench) error { 175 | ctx := context.Background() 176 | data := randomData(1024) 177 | 178 | p := path.Join(testRoot, "read") 179 | meta, err := b.Write(ctx, p, data, backend.Tags{"key": "val"}) 180 | if err != nil { 181 | return err 182 | } 183 | 184 | for !ben.IsTestFinished() { 185 | err := ben.Measure(func() error { 186 | _, err := b.ReadIfModified(ctx, p, meta.Version.Contents) 187 | return err 188 | }) 189 | if err != nil { 190 | return err 191 | } 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func benchSetMetaSame(b backend.Backend, ben *bench.Bench) error { 198 | ctx := context.Background() 199 | data := randomData(1024) 200 | p := path.Join(testRoot, "set-meta") 201 | 202 | meta, err := b.Write(ctx, p, data, backend.Tags{"key": "val"}) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | count := 0 208 | 209 | for !ben.IsTestFinished() { 210 | err := ben.Measure(func() error { 211 | var err error 212 | meta, err = b.SetTagsIf(ctx, p, meta.Version, backend.Tags{ 213 | "key": fmt.Sprintf("val%d", count), 214 | }) 215 | return err 216 | }) 217 | if err != nil { 218 | return err 219 | } 220 | count++ 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func benchGetMeta(b backend.Backend, ben *bench.Bench) error { 227 | ctx := context.Background() 228 | data := randomData(1024) 229 | 230 | p := path.Join(testRoot, "get-meta") 231 | _, err := b.Write(ctx, p, data, backend.Tags{"key": "val"}) 232 | if err != nil { 233 | return err 234 | } 235 | 236 | for !ben.IsTestFinished() { 237 | err := ben.Measure(func() error { 238 | _, err := b.GetMetadata(ctx, p) 239 | return err 240 | }) 241 | if err != nil { 242 | return err 243 | } 244 | } 245 | 246 | return nil 247 | } 248 | 249 | func randomData(size int) []byte { 250 | b := make([]byte, size) 251 | if _, err := rand.Read(b); err != nil { 252 | panic(fmt.Sprintf("Cannot read from random device: %v", err)) 253 | } 254 | return b 255 | } 256 | 257 | func formatMs(d time.Duration) string { 258 | return formatFloat(float64(d) / float64(time.Millisecond)) 259 | } 260 | 261 | func formatFloat(f float64) string { 262 | if f > 1000 { 263 | return fmt.Sprintf("%.2f", f) 264 | } 265 | return fmt.Sprintf("%.4f", f) 266 | } 267 | 268 | func do() error { 269 | b, err := initBackend() 270 | if err != nil { 271 | return err 272 | } 273 | tests := []struct { 274 | name string 275 | fn func(backend.Backend, *bench.Bench) error 276 | }{ 277 | { 278 | name: "WriteSame", 279 | fn: benchWriteSame, 280 | }, 281 | { 282 | name: "WriteFailPre", 283 | fn: benchWriteFailPre, 284 | }, 285 | { 286 | name: "Read", 287 | fn: benchRead, 288 | }, 289 | { 290 | name: "ReadUnchanged", 291 | fn: benchReadUnchanged, 292 | }, 293 | { 294 | name: "SetMetaSame", 295 | fn: benchSetMetaSame, 296 | }, 297 | { 298 | name: "GetMeta", 299 | fn: benchGetMeta, 300 | }, 301 | } 302 | 303 | for _, test := range tests { 304 | if err := runBench(test.name, b, test.fn); err != nil { 305 | return err 306 | } 307 | } 308 | return nil 309 | 310 | } 311 | 312 | func main() { 313 | if err := do(); err != nil { 314 | log.Printf("Error: %v", err) 315 | os.Exit(1) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /hack/benchstat-cmp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2023 The glassdb Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | set -e 19 | 20 | # Compute defaults: last two benchmarks. 21 | DEF=$(ls -ltr docs/profile/bench-* | awk '{ print $9 }' | tail -2) 22 | OLD_DEF=$(echo "${DEF}" | sed -n '1p') 23 | NEW_DEF=$(echo "${DEF}" | sed -n '2p') 24 | 25 | # Otherwise take from args. 26 | case "$#" in 27 | 0) 28 | OLD="${OLD_DEF}" 29 | NEW="${NEW_DEF}" 30 | ;; 31 | 1) 32 | OLD="${OLD_DEF}" 33 | NEW="${1}" 34 | ;; 35 | *) 36 | OLD="${1}" 37 | NEW="${2}" 38 | ;; 39 | esac 40 | 41 | echo "Comparing old=${OLD} new=${NEW}" 42 | benchstat <(grep -v 'Stats:' "${OLD}" | grep '^Benchmark') \ 43 | <(grep -v 'Stats:' "${NEW}" | grep '^Benchmark') 44 | -------------------------------------------------------------------------------- /hack/benchstat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2023 The glassdb Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # See https://pkg.go.dev/golang.org/x/perf/cmd/benchstat 19 | 20 | set -e 21 | set -o pipefail 22 | 23 | PATTERN=${1:-.} 24 | 25 | cd $(dirname $(readlink -f $0))/.. 26 | STATS=$(mktemp) 27 | 28 | echo "Saving results to ${STATS}" 29 | 30 | for i in {1..10}; do 31 | echo "Benchmark $i/10" 32 | go test -benchmem -bench "${PATTERN}" --print-stats | tee -a "${STATS}" 33 | done 34 | 35 | benchstat "${STATS}" 36 | echo "All runs saved in ${STATS}" 37 | -------------------------------------------------------------------------------- /hack/debug/logs2csv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'time,tx,msg,path,args,res,err' 4 | jq -s --raw-output '.[] | [.time, .tx, .msg, .path, .args, .res, .err] | @csv' 5 | -------------------------------------------------------------------------------- /hack/pprof.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2023 The glassdb Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | set -e 19 | 20 | PATTERN=${1:-.} 21 | 22 | cd $(dirname $(readlink -f $0))/.. 23 | 24 | go test -test.v -bench "${PATTERN}" -cpuprofile=docs/profile/cpu.prof 25 | go test -test.v -benchmem -bench "${PATTERN}" -memprofile=docs/profile/mem.prof 26 | go test -test.v -tags tracing -bench "${PATTERN}" -trace=docs/profile/trace.out 27 | go tool pprof -http=":8081" glassdb.test docs/profile/cpu.prof 28 | go tool pprof -http=":8081" glassdb.test docs/profile/mem.prof 29 | go tool trace docs/profile/trace.out 30 | -------------------------------------------------------------------------------- /hack/test-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2023 The glassdb Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | set -xe 19 | 20 | go test -timeout=30s ./... 21 | go test -timeout=120s -race ./... 22 | go test -timeout=30s -count=10 ./internal/trans 23 | go test -timeout=120s -count=10 . --debug-logs 24 | -------------------------------------------------------------------------------- /internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cache 16 | 17 | import ( 18 | "container/list" 19 | "sync" 20 | ) 21 | 22 | func New(maxSizeB int) *Cache { 23 | return &Cache{ 24 | maxSizeB: maxSizeB, 25 | entries: make(map[string]*list.Element), 26 | evicts: list.New(), 27 | } 28 | } 29 | 30 | type Value interface { 31 | SizeB() int 32 | } 33 | 34 | type Cache struct { 35 | m sync.Mutex 36 | maxSizeB int 37 | currSizeB int 38 | entries map[string]*list.Element 39 | evicts *list.List 40 | } 41 | 42 | func (c *Cache) Get(key string) (Value, bool) { 43 | c.m.Lock() 44 | defer c.m.Unlock() 45 | if e, ok := c.entries[key]; ok { 46 | c.evicts.MoveToFront(e) 47 | return e.Value.(entry).value, true 48 | } 49 | return nil, false 50 | } 51 | 52 | func (c *Cache) Set(key string, val Value) { 53 | c.Update(key, func(Value) Value { 54 | return val 55 | }) 56 | } 57 | 58 | // Update updates the given cache value while locked. 59 | // If the key is not present, nil is passed to fn. To remove 60 | // the value, return nil in fn. 61 | func (c *Cache) Update(key string, fn func(v Value) Value) { 62 | c.m.Lock() 63 | defer c.m.Unlock() 64 | 65 | if e, ok := c.entries[key]; ok { 66 | c.evicts.MoveToFront(e) 67 | old := e.Value.(entry) 68 | newv := fn(old.value) 69 | if newv == nil { 70 | // We had a value and now it's gone. 71 | c.deleteEntry(key, e) 72 | return 73 | } 74 | c.currSizeB += newv.SizeB() - old.value.SizeB() 75 | e.Value = entry{key: key, value: newv} 76 | } else { 77 | newv := fn(nil) 78 | if newv == nil { 79 | // Nothing happened. We had nothing, we got nothing. 80 | return 81 | } 82 | e := c.evicts.PushFront(entry{key: key, value: newv}) 83 | c.entries[key] = e 84 | c.currSizeB += newv.SizeB() 85 | } 86 | 87 | c.removeOldest() 88 | } 89 | 90 | func (c *Cache) Delete(key string) { 91 | c.m.Lock() 92 | defer c.m.Unlock() 93 | 94 | if e, ok := c.entries[key]; ok { 95 | c.deleteEntry(key, e) 96 | } 97 | } 98 | 99 | func (c *Cache) SizeB() int { 100 | c.m.Lock() 101 | defer c.m.Unlock() 102 | return c.currSizeB 103 | } 104 | 105 | func (c *Cache) deleteEntry(key string, e *list.Element) { 106 | ent := e.Value.(entry) 107 | c.currSizeB -= ent.value.SizeB() 108 | c.evicts.Remove(e) 109 | delete(c.entries, key) 110 | } 111 | 112 | func (c *Cache) removeOldest() { 113 | for c.currSizeB > c.maxSizeB { 114 | it := c.evicts.Back() 115 | if it == nil { 116 | return 117 | } 118 | ent := it.Value.(entry) 119 | c.currSizeB -= ent.value.SizeB() 120 | delete(c.entries, ent.key) 121 | c.evicts.Remove(it) 122 | } 123 | } 124 | 125 | type entry struct { 126 | key string 127 | value Value 128 | } 129 | -------------------------------------------------------------------------------- /internal/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cache 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | type testEntry string 24 | 25 | func (e testEntry) SizeB() int { 26 | return len(e) 27 | } 28 | 29 | func TestGetSet(t *testing.T) { 30 | c := New(100) 31 | assert.Zero(t, c.SizeB()) 32 | 33 | // Set and get. 34 | c.Set("a", testEntry("foo")) 35 | got, ok := c.Get("a") 36 | assert.True(t, ok) 37 | assert.Equal(t, got, testEntry("foo")) 38 | assert.Equal(t, 3, c.SizeB()) 39 | 40 | // Modify and get again. 41 | c.Set("a", testEntry("barbaz")) 42 | got, ok = c.Get("a") 43 | assert.True(t, ok) 44 | assert.Equal(t, got, testEntry("barbaz")) 45 | assert.Equal(t, 6, c.SizeB()) 46 | } 47 | 48 | func TestDelete(t *testing.T) { 49 | c := New(100) 50 | keys := []string{"k1", "k2"} 51 | 52 | // Set and get all keys. 53 | for _, key := range keys { 54 | c.Set(key, testEntry(key)) 55 | } 56 | for _, key := range keys { 57 | _, ok := c.Get(key) 58 | assert.True(t, ok) 59 | } 60 | assert.Equal(t, 4, c.SizeB()) 61 | 62 | // Delete one key. 63 | c.Delete(keys[0]) 64 | // One is deleted, the other is still there. 65 | _, ok := c.Get(keys[0]) 66 | assert.False(t, ok) 67 | _, ok = c.Get(keys[1]) 68 | assert.True(t, ok) 69 | assert.Equal(t, 2, c.SizeB()) 70 | } 71 | 72 | func TestUpdate(t *testing.T) { 73 | c := New(100) 74 | c.Set("a", testEntry("foo")) 75 | assert.Equal(t, 3, c.SizeB()) 76 | 77 | c.Update("a", func(old Value) Value { 78 | return testEntry("barbaz") 79 | }) 80 | got, ok := c.Get("a") 81 | assert.True(t, ok) 82 | assert.Equal(t, got, testEntry("barbaz")) 83 | assert.Equal(t, 6, c.SizeB()) 84 | } 85 | 86 | func TestUpdateNew(t *testing.T) { 87 | c := New(100) 88 | c.Update("a", func(old Value) Value { 89 | assert.Nil(t, old) 90 | return testEntry("bar") 91 | }) 92 | got, ok := c.Get("a") 93 | assert.True(t, ok) 94 | assert.Equal(t, got, testEntry("bar")) 95 | assert.Equal(t, 3, c.SizeB()) 96 | } 97 | 98 | func TestUpdateDelete(t *testing.T) { 99 | c := New(100) 100 | c.Set("a", testEntry("foo")) 101 | assert.Equal(t, 3, c.SizeB()) 102 | 103 | c.Update("a", func(old Value) Value { 104 | return nil 105 | }) 106 | _, ok := c.Get("a") 107 | assert.False(t, ok) 108 | assert.Zero(t, c.SizeB()) 109 | } 110 | 111 | func TestUpdateNope(t *testing.T) { 112 | c := New(100) 113 | c.Update("a", func(old Value) Value { 114 | assert.Nil(t, old) 115 | return nil 116 | }) 117 | assert.Zero(t, c.SizeB()) 118 | } 119 | -------------------------------------------------------------------------------- /internal/concurr/background.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "sync" 20 | 21 | "github.com/sourcegraph/conc" 22 | ) 23 | 24 | func NewBackground() *Background { 25 | return &Background{ 26 | done: make(chan struct{}), 27 | } 28 | } 29 | 30 | type Background struct { 31 | wg conc.WaitGroup 32 | done chan struct{} 33 | closed bool 34 | m sync.Mutex 35 | } 36 | 37 | func (b *Background) Close() { 38 | b.m.Lock() 39 | if b.closed { 40 | b.m.Unlock() 41 | return 42 | } 43 | 44 | close(b.done) 45 | b.closed = true 46 | b.m.Unlock() 47 | b.wg.Wait() 48 | } 49 | 50 | func (b *Background) Go(ctx context.Context, fn func(context.Context)) bool { 51 | b.m.Lock() 52 | defer b.m.Unlock() 53 | 54 | if b.closed { 55 | return false 56 | } 57 | b.wg.Go(func() { 58 | fn(ContextWithNewCancel(ctx, b.done)) 59 | }) 60 | return true 61 | } 62 | -------------------------------------------------------------------------------- /internal/concurr/background_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "sync/atomic" 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestBackgroundRun(t *testing.T) { 27 | ctx := context.Background() 28 | b := NewBackground() 29 | ran := false 30 | ok := b.Go(ctx, func(context.Context) { 31 | ran = true 32 | }) 33 | assert.True(t, ok) 34 | b.Close() 35 | assert.True(t, ran) 36 | assert.False(t, b.Go(ctx, func(context.Context) {})) 37 | } 38 | 39 | func TestBackgroundConcurrent(t *testing.T) { 40 | ctx := context.Background() 41 | b := NewBackground() 42 | ch1 := make(chan struct{}) 43 | ch2 := make(chan struct{}) 44 | count := int32(0) 45 | ok1 := b.Go(ctx, func(context.Context) { 46 | <-ch1 47 | ch2 <- struct{}{} 48 | // We can reach this only if the two routines run in parallel. 49 | atomic.AddInt32(&count, 1) 50 | }) 51 | ok2 := b.Go(ctx, func(context.Context) { 52 | ch1 <- struct{}{} 53 | <-ch2 54 | atomic.AddInt32(&count, 1) 55 | }) 56 | b.Close() 57 | 58 | assert.True(t, ok1) 59 | assert.True(t, ok2) 60 | assert.Equal(t, 2, int(count)) 61 | } 62 | 63 | func TestBackgroundCancel(t *testing.T) { 64 | ctx := context.Background() 65 | b := NewBackground() 66 | ch := make(chan struct{}) 67 | done := false 68 | 69 | b.Go(ctx, func(ctx context.Context) { 70 | close(ch) // Signal that we started running. 71 | // Wait for the context to be canceled. 72 | <-ctx.Done() 73 | time.Sleep(5 * time.Millisecond) 74 | done = true 75 | }) 76 | 77 | // Wait for the task to start. 78 | <-ch 79 | 80 | b.Close() 81 | // Make sure the context in the task is canceled, and 82 | // the task can complete successfully. 83 | assert.True(t, done) 84 | 85 | // New tasks can't start. 86 | ok := b.Go(ctx, func(context.Context) {}) 87 | assert.False(t, ok) 88 | } 89 | 90 | func TestNestedBackground(t *testing.T) { 91 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 92 | defer cancel() 93 | b := NewBackground() 94 | done := make(chan struct{}) 95 | 96 | b.Go(ctx, func(context.Context) { 97 | b.Go(ctx, func(context.Context) { 98 | close(done) 99 | }) 100 | }) 101 | 102 | select { 103 | case <-done: 104 | case <-ctx.Done(): 105 | t.Errorf("should have reached done before") 106 | } 107 | b.Close() 108 | } 109 | -------------------------------------------------------------------------------- /internal/concurr/backoff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "time" 21 | 22 | "github.com/cenkalti/backoff/v4" 23 | "github.com/jonboulle/clockwork" 24 | ) 25 | 26 | // TODO: Make these configurable. Compute them based on backend latency. 27 | const ( 28 | initialInterval = 200 * time.Millisecond 29 | maxInterval = 5 * time.Second 30 | ) 31 | 32 | func Permanent(err error) error { 33 | return backoff.Permanent(err) 34 | } 35 | 36 | func IsPermanent(err error) bool { 37 | var perr *backoff.PermanentError 38 | return errors.As(err, &perr) 39 | } 40 | 41 | type Retrier struct { 42 | backoff.BackOff 43 | clock clockwork.Clock 44 | } 45 | 46 | func RetryOptions(initial, max time.Duration, c clockwork.Clock) Retrier { 47 | b := backoff.NewExponentialBackOff() 48 | b.Clock = c 49 | b.InitialInterval = initial 50 | b.MaxInterval = max 51 | b.MaxElapsedTime = 0 52 | return Retrier{b, c} 53 | } 54 | 55 | func (r Retrier) Retry(ctx context.Context, fn func() error) error { 56 | return backoff.RetryNotifyWithTimer(fn, 57 | backoff.WithContext(r.BackOff, ctx), nil, &timer{r.clock, nil}) 58 | } 59 | 60 | func RetryWithBackoff(ctx context.Context, c clockwork.Clock, f func() error) error { 61 | r := RetryOptions(initialInterval, maxInterval, c) 62 | return backoff.RetryNotifyWithTimer(f, 63 | backoff.WithContext(r.BackOff, ctx), nil, &timer{c, nil}) 64 | } 65 | 66 | type timer struct { 67 | clockwork.Clock 68 | timer clockwork.Timer 69 | } 70 | 71 | func (t *timer) C() <-chan time.Time { 72 | return t.timer.Chan() 73 | } 74 | 75 | // Start starts the timer to fire after the given duration 76 | func (t *timer) Start(duration time.Duration) { 77 | if t.timer == nil { 78 | t.timer = t.Clock.NewTimer(duration) 79 | } else { 80 | t.timer.Reset(duration) 81 | } 82 | } 83 | 84 | // Stop is called when the timer is not used anymore and resources may be freed. 85 | func (t timer) Stop() { 86 | if t.timer != nil { 87 | t.timer.Stop() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/concurr/chan.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | // MakeChainInfCap simulates a channel with infinite capacity. This will never 18 | // block the sender and grow an internal buffer if necessary. 19 | // 20 | // Close the input channel when finished, to free resources. 21 | func MakeChanInfCap[T any](expectedCap int) (<-chan T, chan<- T) { 22 | in := make(chan T, expectedCap) 23 | out := make(chan T, expectedCap) 24 | var queue []T 25 | 26 | go func() { 27 | loop: 28 | for { 29 | // Nothing to output here. We need to wait for some input. 30 | v, ok := <-in 31 | if !ok { 32 | // We're closing. Just exit (the queue is empty). 33 | break loop 34 | } 35 | // Try to push the element directly to the out chan. 36 | // If it's full, push it to the queue instead. 37 | select { 38 | case out <- v: 39 | continue loop 40 | default: 41 | queue = append(queue, v) 42 | } 43 | 44 | // Here we have something in the queue. 45 | for len(queue) > 0 { 46 | select { 47 | case v, ok := <-in: 48 | if !ok { 49 | // We're closing. Consume the whole queue and exit. 50 | for _, e := range queue { 51 | out <- e 52 | } 53 | break loop 54 | } 55 | queue = append(queue, v) 56 | case out <- queue[0]: 57 | var empty T 58 | queue[0] = empty // Avoid possible leaks. 59 | queue = queue[1:] 60 | } 61 | } 62 | } 63 | 64 | // Signal the end of things. 65 | close(out) 66 | }() 67 | 68 | return out, in 69 | } 70 | -------------------------------------------------------------------------------- /internal/concurr/chan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "golang.org/x/sync/errgroup" 23 | ) 24 | 25 | func TestChanInfCap(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | waitSend bool 29 | waitRecv bool 30 | }{ 31 | { 32 | name: "no wait", 33 | }, 34 | { 35 | name: "buffer at send", 36 | waitRecv: true, 37 | }, 38 | { 39 | name: "buffer at recv", 40 | waitSend: true, 41 | }, 42 | } 43 | 44 | wait := func(ok bool) { 45 | if !ok { 46 | return 47 | } 48 | time.Sleep(time.Millisecond) 49 | } 50 | 51 | for _, tc := range tests { 52 | t.Run(tc.name, func(t *testing.T) { 53 | out, in := MakeChanInfCap[int](1) 54 | var rec []int 55 | 56 | g := errgroup.Group{} 57 | g.Go(func() error { 58 | for v := range out { 59 | wait(tc.waitRecv) 60 | rec = append(rec, v) 61 | } 62 | return nil 63 | }) 64 | 65 | for i := 0; i < 100; i++ { 66 | wait(tc.waitSend) 67 | in <- i 68 | } 69 | close(in) 70 | _ = g.Wait() 71 | 72 | assert.Len(t, rec, 100) 73 | }) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /internal/concurr/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "github.com/jonboulle/clockwork" 22 | ) 23 | 24 | func ContextWithTimeout( 25 | parent context.Context, 26 | clock clockwork.Clock, 27 | timeout time.Duration, 28 | ) (context.Context, context.CancelFunc) { 29 | ctx, cancel := context.WithCancel(parent) 30 | clock.AfterFunc(timeout, func() { 31 | cancel() 32 | }) 33 | return ctx, cancel 34 | } 35 | 36 | // ContextWithNewCancel replaces the parent cancellation (if any) with the 37 | // given one, while preserving the embedded values. 38 | func ContextWithNewCancel(parent context.Context, done <-chan struct{}) context.Context { 39 | return detachedCtx{ 40 | parent: parent, 41 | done: done, 42 | } 43 | } 44 | 45 | type detachedCtx struct { 46 | parent context.Context 47 | done <-chan struct{} 48 | } 49 | 50 | func (v detachedCtx) Deadline() (time.Time, bool) { return time.Time{}, false } 51 | func (v detachedCtx) Done() <-chan struct{} { return v.done } 52 | func (v detachedCtx) Value(key interface{}) interface{} { return v.parent.Value(key) } 53 | 54 | func (v detachedCtx) Err() error { 55 | select { 56 | case <-v.done: 57 | return context.Canceled 58 | default: 59 | return nil 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/concurr/context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "github.com/jonboulle/clockwork" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestContextTimeout(t *testing.T) { 27 | clock := clockwork.NewFakeClock() 28 | ctx, cancel := ContextWithTimeout(context.Background(), clock, 10*time.Second) 29 | defer cancel() 30 | 31 | go func() { 32 | for { 33 | clock.BlockUntil(1) 34 | clock.Advance(time.Second) 35 | time.Sleep(time.Millisecond) 36 | } 37 | }() 38 | 39 | // The context wasn't canceled yet. 40 | clock.Sleep(time.Second) 41 | assert.NoError(t, ctx.Err()) 42 | 43 | // The context was definitely already canceled. 44 | clock.Sleep(10 * time.Second) 45 | time.Sleep(time.Millisecond) // Wait for things to wake up. 46 | assert.ErrorIs(t, ctx.Err(), context.Canceled) 47 | } 48 | 49 | type testKey string 50 | 51 | func TestContextCancel(t *testing.T) { 52 | parent := context.WithValue(context.Background(), testKey("testKey"), "foo") 53 | parent, cancel := context.WithCancel(parent) 54 | done := make(chan struct{}) 55 | ctx := ContextWithNewCancel(parent, done) 56 | assert.NoError(t, parent.Err()) 57 | 58 | // Values of the parent are preserved. 59 | gotVal := ctx.Value(testKey("testKey")).(string) 60 | assert.Equal(t, "foo", gotVal) 61 | 62 | // Cancelling the parent doesn't affect the child. 63 | cancel() 64 | assert.ErrorIs(t, parent.Err(), context.Canceled) 65 | assert.NoError(t, ctx.Err()) 66 | select { 67 | case <-ctx.Done(): 68 | assert.True(t, false, "should be unreachable") 69 | default: 70 | // All good here. 71 | } 72 | 73 | // Closing the 'done' channel causes cancellation. 74 | close(done) 75 | assert.ErrorIs(t, ctx.Err(), context.Canceled) 76 | select { 77 | case <-ctx.Done(): 78 | default: 79 | assert.True(t, false, "should be unreachable") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/concurr/dedup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "sync" 20 | ) 21 | 22 | func NewDedup(w Worker) *Dedup { 23 | return &Dedup{ 24 | work: w, 25 | contr: controller{ 26 | calls: make(map[string]*call), 27 | }, 28 | } 29 | } 30 | 31 | type Dedup struct { 32 | work Worker 33 | contr controller 34 | } 35 | 36 | func (d *Dedup) Do(ctx context.Context, key string, r Request) error { 37 | return d.contr.Do(ctx, key, r, d.work) 38 | } 39 | 40 | type Request interface { 41 | Merge(other Request) (Request, bool) 42 | CanReorder() bool 43 | } 44 | 45 | type Worker interface { 46 | Work(ctx context.Context, key string, cntr DedupContr) error 47 | } 48 | 49 | type DedupContr interface { 50 | Request(key string) Request 51 | OnNextDo(key string) <-chan struct{} 52 | } 53 | 54 | type controller struct { 55 | calls map[string]*call 56 | m sync.Mutex 57 | } 58 | 59 | func (d *controller) Do(ctx context.Context, key string, r Request, w Worker) error { 60 | var err error 61 | 62 | d.m.Lock() 63 | c, inprog := d.calls[key] 64 | if !inprog { 65 | c = &call{ 66 | Curr: requestBundle{ 67 | Main: requestCtx{ 68 | Ctx: ctx, 69 | Request: r, 70 | }, 71 | }, 72 | } 73 | d.calls[key] = c 74 | d.m.Unlock() 75 | 76 | err = w.Work(ctx, key, d) 77 | } else { 78 | notifyCh := make(chan callResult, 1) 79 | rctx := requestCtx{ 80 | Ctx: ctx, 81 | Request: r, 82 | NotifyCh: notifyCh, 83 | } 84 | if r.CanReorder() { 85 | c.Pending = append(c.Pending, rctx) 86 | } else { 87 | c.Queue = append(c.Queue, rctx) 88 | } 89 | // Notify workers waiting on OnNextDo. 90 | // Avoid deadlocks when the channel is full. Only one notification is 91 | // necessary. 92 | // TODO: Add unit test for this case. 93 | if c.NextCh != nil { 94 | select { 95 | case c.NextCh <- struct{}{}: 96 | default: 97 | } 98 | } 99 | d.m.Unlock() 100 | 101 | select { 102 | case <-ctx.Done(): 103 | return ctx.Err() 104 | case n := <-notifyCh: 105 | if !n.IsNext { 106 | return n.Err 107 | } 108 | err = w.Work(ctx, key, d) 109 | // Fall through the request handling logic. 110 | } 111 | } 112 | 113 | d.m.Lock() 114 | // The main request completed. 115 | c.Curr.Main = requestCtx{} 116 | 117 | if err == nil || ctx.Err() == nil { 118 | // If the context expired, do not notify the whole bundle. 119 | // Some other request in there could pick up the request. 120 | d.notifyBundle(c.Curr, err) 121 | c.Curr = requestBundle{} 122 | } 123 | if c.NextCh != nil { 124 | // After the main call completed, we know there's no one waiting for 125 | // the next Do call. Avoid unnecessary queuing by getting rid of the 126 | // channel now. 127 | c.NextCh = nil 128 | } 129 | d.wakeUpNext(key, c) 130 | d.m.Unlock() 131 | 132 | return err 133 | } 134 | 135 | func (d *controller) Request(key string) Request { 136 | d.m.Lock() 137 | defer d.m.Unlock() 138 | c := d.calls[key] 139 | 140 | c.Curr.Other = filterExpiredReqs(c.Curr.Other) 141 | c.Pending = filterExpiredReqs(c.Pending) 142 | c.Queue = filterExpiredReqs(c.Queue) 143 | 144 | // Start by reconstructing the current bundle. 145 | newReq := c.Curr.Main.Request 146 | for _, r := range c.Curr.Other { 147 | // Requests in the bundle are mergeable by definition. 148 | var ok bool 149 | newReq, ok = newReq.Merge(r.Request) 150 | if !ok { 151 | panic("dedup: unexpected non-mergeable request in the bundle") 152 | } 153 | } 154 | 155 | // Merge pending requests. Any mergeable will join the bundle. 156 | i := 0 157 | for _, r := range c.Pending { 158 | mr, ok := newReq.Merge(r.Request) 159 | if !ok { 160 | c.Pending[i] = r 161 | i++ 162 | continue 163 | } 164 | newReq = mr 165 | c.Curr.Other = append(c.Curr.Other, r) 166 | } 167 | c.Pending = c.Pending[:i] 168 | 169 | // Merge from the queue as well. 170 | // However, since the queue is ordered, stop the first moment we can't 171 | // merge. 172 | i = 0 173 | for _, r := range c.Queue { 174 | mr, ok := newReq.Merge(r.Request) 175 | if !ok { 176 | break 177 | } 178 | newReq = mr 179 | c.Curr.Other = append(c.Curr.Other, r) 180 | i++ 181 | } 182 | c.Queue = c.Queue[i:] 183 | 184 | return newReq 185 | } 186 | 187 | func (d *controller) OnNextDo(key string) <-chan struct{} { 188 | d.m.Lock() 189 | defer d.m.Unlock() 190 | 191 | c := d.calls[key] 192 | if c.NextCh == nil { 193 | c.NextCh = make(chan struct{}, 3) 194 | } 195 | return c.NextCh 196 | } 197 | 198 | func (d *controller) notifyBundle(bundle requestBundle, err error) { 199 | for _, r := range bundle.Other { 200 | r.NotifyCh <- callResult{Err: err} 201 | } 202 | } 203 | 204 | func (d *controller) wakeUpNext(key string, c *call) { 205 | c.Curr.Other = filterExpiredReqs(c.Curr.Other) 206 | c.Pending = filterExpiredReqs(c.Pending) 207 | c.Queue = filterExpiredReqs(c.Queue) 208 | 209 | switch { 210 | case len(c.Curr.Other) > 0: 211 | c.Curr.Main = c.Curr.Other[0] 212 | c.Curr.Other = c.Curr.Other[1:] 213 | case len(c.Queue) > 0: 214 | c.Curr.Main = c.Queue[0] 215 | c.Queue = c.Queue[1:] 216 | case len(c.Pending) > 0: 217 | c.Curr.Main = c.Pending[0] 218 | c.Pending = c.Pending[1:] 219 | default: 220 | // Nothing to wake up. Cleanup. 221 | delete(d.calls, key) 222 | return 223 | } 224 | 225 | // Wake up the new main request. 226 | c.Curr.Main.NotifyCh <- callResult{IsNext: true} 227 | } 228 | 229 | type call struct { 230 | Curr requestBundle 231 | Pending []requestCtx 232 | Queue []requestCtx 233 | NextCh chan struct{} 234 | } 235 | 236 | type requestCtx struct { 237 | Ctx context.Context 238 | Request Request 239 | NotifyCh chan callResult 240 | } 241 | 242 | type callResult struct { 243 | Err error 244 | IsNext bool 245 | } 246 | 247 | type requestBundle struct { 248 | Main requestCtx 249 | Other []requestCtx 250 | } 251 | 252 | func filterExpiredReqs(rs []requestCtx) []requestCtx { 253 | i := 0 254 | for _, r := range rs { 255 | if r.Ctx.Err() == nil { 256 | rs[i] = r 257 | i++ 258 | } 259 | } 260 | return rs[:i] 261 | } 262 | -------------------------------------------------------------------------------- /internal/concurr/dedup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "golang.org/x/sync/errgroup" 23 | ) 24 | 25 | func TestSingleCall(t *testing.T) { 26 | tw := &testWorker{} 27 | d := NewDedup(tw) 28 | err := d.Do(context.Background(), "key", testRequest{}) 29 | assert.NoError(t, err) 30 | assert.Equal(t, 1, tw.counter) 31 | } 32 | 33 | func TestContextExpired(t *testing.T) { 34 | tw := &testWorker{} 35 | d := NewDedup(tw) 36 | ctx, cancel := context.WithCancel(context.Background()) 37 | cancel() 38 | err := d.Do(ctx, "key", testRequest{}) 39 | assert.ErrorIs(t, err, ctx.Err()) 40 | assert.Equal(t, 1, tw.counter) 41 | } 42 | 43 | func TestMergeDo(t *testing.T) { 44 | tw := &testMergeWorker{waitRequests: 1} 45 | ctx := context.Background() 46 | d := NewDedup(tw) 47 | wg := errgroup.Group{} 48 | wg.Go(func() error { 49 | return d.Do(ctx, "key", mergeableRequest(1)) 50 | }) 51 | err := d.Do(ctx, "key", mergeableRequest(1)) 52 | assert.NoError(t, err) 53 | err = wg.Wait() 54 | assert.NoError(t, err) 55 | assert.Equal(t, []int{2}, tw.res) 56 | } 57 | 58 | func TestSequentialDo(t *testing.T) { 59 | tw := &testMergeWorker{waitRequests: 1} 60 | ctx := context.Background() 61 | d := NewDedup(tw) 62 | wg := errgroup.Group{} 63 | wg.Go(func() error { 64 | return d.Do(ctx, "key", unmergeableRequest(1)) 65 | }) 66 | err := d.Do(ctx, "key", mergeableRequest(1)) 67 | assert.NoError(t, err) 68 | err = wg.Wait() 69 | assert.NoError(t, err) 70 | assert.Equal(t, []int{1, 1}, tw.res) 71 | } 72 | 73 | func TestReorderMerge(t *testing.T) { 74 | tw := &testMergeWorker{waitRequests: 2} 75 | ctx := context.Background() 76 | d := NewDedup(tw) 77 | wg := errgroup.Group{} 78 | wg.Go(func() error { 79 | return d.Do(ctx, "key", unmergeableRequest(2)) 80 | }) 81 | wg.Go(func() error { 82 | return d.Do(ctx, "key", reorderableRequest(3)) 83 | }) 84 | err := d.Do(ctx, "key", mergeableRequest(5)) 85 | assert.NoError(t, err) 86 | err = wg.Wait() 87 | assert.NoError(t, err) 88 | assert.Equal(t, []int{8, 2}, tw.res) 89 | } 90 | 91 | type testWorker struct { 92 | counter int 93 | } 94 | 95 | func (t *testWorker) Work(ctx context.Context, key string, cntr DedupContr) error { 96 | _ = cntr.Request(key) 97 | t.counter++ 98 | return ctx.Err() 99 | } 100 | 101 | type testMergeWorker struct { 102 | waitRequests int 103 | res []int 104 | } 105 | 106 | func (t *testMergeWorker) Work(ctx context.Context, key string, cntr DedupContr) error { 107 | for t.waitRequests > 0 { 108 | select { 109 | case <-cntr.OnNextDo(key): 110 | case <-ctx.Done(): 111 | return ctx.Err() 112 | } 113 | t.waitRequests-- 114 | } 115 | 116 | r := cntr.Request(key).(testRequest) 117 | t.res = append(t.res, r.counter) 118 | return nil 119 | } 120 | 121 | func mergeableRequest(counter int) testRequest { 122 | return testRequest{counter: counter, canMerge: true} 123 | } 124 | 125 | func unmergeableRequest(counter int) testRequest { 126 | return testRequest{counter: counter} 127 | } 128 | 129 | func reorderableRequest(counter int) testRequest { 130 | return testRequest{counter: counter, canMerge: true, canReorder: true} 131 | } 132 | 133 | type testRequest struct { 134 | counter int 135 | canMerge bool 136 | canReorder bool 137 | } 138 | 139 | func (r testRequest) CanReorder() bool { return r.canReorder } 140 | 141 | func (r testRequest) Merge(other Request) (Request, bool) { 142 | or, ok := other.(testRequest) 143 | if !ok { 144 | return nil, false 145 | } 146 | if !r.canMerge || !or.canMerge { 147 | return nil, false 148 | } 149 | return mergeableRequest(r.counter + or.counter), true 150 | } 151 | -------------------------------------------------------------------------------- /internal/concurr/fanout.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | 20 | "golang.org/x/sync/errgroup" 21 | ) 22 | 23 | func NewFanout(maxConcurrent int) Fanout { 24 | return Fanout{ 25 | limit: maxConcurrent, 26 | } 27 | } 28 | 29 | type Fanout struct { 30 | limit int 31 | } 32 | 33 | func (o Fanout) Spawn(ctx context.Context, num int, f func(context.Context, int) error) Result { 34 | g, ctx := errgroup.WithContext(ctx) 35 | g.SetLimit(o.limit) 36 | 37 | for i := 0; i < num; i++ { 38 | i := i // https://golang.org/doc/faq#closures_and_goroutines 39 | g.Go(func() error { 40 | return f(ctx, i) 41 | }) 42 | } 43 | 44 | return g 45 | } 46 | 47 | type Result interface { 48 | Wait() error 49 | } 50 | -------------------------------------------------------------------------------- /internal/concurr/fanout_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package concurr 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "github.com/jonboulle/clockwork" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestFanout(t *testing.T) { 27 | ctx := context.Background() 28 | f := NewFanout(3) 29 | v := make([]bool, 3) 30 | res := f.Spawn(ctx, 3, func(ctx context.Context, i int) error { 31 | v[i] = true 32 | return nil 33 | }) 34 | assert.NoError(t, res.Wait()) 35 | for _, b := range v { 36 | assert.True(t, b) 37 | } 38 | } 39 | 40 | func TestParallel(t *testing.T) { 41 | ctx := context.Background() 42 | clock := clockwork.NewFakeClock() 43 | f := NewFanout(3) 44 | 45 | r1 := f.Spawn(ctx, 2, func(context.Context, int) error { 46 | clock.Sleep(100 * time.Millisecond) 47 | return nil 48 | }) 49 | r2 := f.Spawn(ctx, 1, func(context.Context, int) error { 50 | clock.Sleep(100 * time.Millisecond) 51 | return nil 52 | }) 53 | // Block until the goroutines start waiting. 54 | clock.BlockUntil(3) 55 | // Give enough time to run in parallel but not in series. 56 | clock.Advance(110 * time.Millisecond) 57 | 58 | assert.NoError(t, r1.Wait()) 59 | assert.NoError(t, r2.Wait()) 60 | } 61 | 62 | func TestLimit(t *testing.T) { 63 | ctx := context.Background() 64 | // We need a real clock, because it's hard to avoid race conditions 65 | // in the test with the fake clock. 66 | clock := clockwork.NewRealClock() 67 | f := NewFanout(2) 68 | 69 | start := clock.Now() 70 | v := make([]time.Duration, 3) 71 | r := f.Spawn(ctx, 3, func(_ context.Context, i int) error { 72 | clock.Sleep(50 * time.Millisecond) 73 | v[i] = clock.Since(start) 74 | return nil 75 | }) 76 | 77 | assert.NoError(t, r.Wait()) 78 | assert.Less(t, v[0], 90*time.Millisecond) 79 | assert.Less(t, v[1], 90*time.Millisecond) 80 | assert.Greater(t, v[2], 90*time.Millisecond) 81 | } 82 | -------------------------------------------------------------------------------- /internal/data/paths/paths.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package paths 16 | 17 | import ( 18 | "bytes" 19 | "encoding/base64" 20 | "fmt" 21 | "path" 22 | "strings" 23 | "sync" 24 | 25 | "github.com/mbrt/glassdb/internal/data" 26 | ) 27 | 28 | type Type string 29 | 30 | const ( 31 | UnknownType Type = "" 32 | KeyType Type = "_k" 33 | CollectionType Type = "_c" 34 | TransactionType Type = "_t" 35 | CollectionInfoType Type = "_i" 36 | ) 37 | 38 | const encodeAlphabet = "0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" 39 | 40 | // This base64 encoding preserves the lexicographical ordering. 41 | var encoding = base64.NewEncoding(encodeAlphabet).WithPadding(base64.NoPadding) 42 | 43 | var bufPool = sync.Pool{ 44 | New: func() any { 45 | return &bytes.Buffer{} 46 | }, 47 | } 48 | 49 | func FromKey(prefix string, key []byte) string { 50 | return prefixEncode(prefix, KeyType, key) 51 | } 52 | 53 | func ToKey(suffix string) ([]byte, error) { 54 | return decode(KeyType, suffix) 55 | } 56 | 57 | func KeysPrefix(prefix string) string { 58 | return typedPrefix(prefix, KeyType) 59 | } 60 | 61 | func FromCollection(prefix string, name []byte) string { 62 | return prefixEncode(prefix, CollectionType, name) 63 | } 64 | 65 | func ToCollection(suffix string) ([]byte, error) { 66 | return decode(CollectionType, suffix) 67 | } 68 | 69 | func CollectionInfo(prefix string) string { 70 | return path.Join(prefix, "_i") 71 | } 72 | 73 | func IsCollectionInfo(p string) bool { 74 | return strings.HasSuffix(p, "/_i") 75 | } 76 | 77 | func CollectionsPrefix(prefix string) string { 78 | return typedPrefix(prefix, CollectionType) 79 | } 80 | 81 | func FromTransaction(prefix string, id data.TxID) string { 82 | return prefixEncode(prefix, TransactionType, id) 83 | } 84 | 85 | func ToTransaction(suffix string) (data.TxID, error) { 86 | return decode(TransactionType, suffix) 87 | } 88 | 89 | func Parse(p string) (ParseResult, error) { 90 | // Collection info is a special case. 91 | // Example path: foo/bar/_i -> prefix: foo/bar, suffix: "" 92 | if IsCollectionInfo(p) { 93 | return ParseResult{ 94 | Prefix: p[:len(p)-3], 95 | Type: CollectionInfoType, 96 | }, nil 97 | } 98 | 99 | // The path is composed by three parts: 100 | // prefix, type, suffix; separated by /. 101 | prefixIdx, typeIdx := pathPartsIndexes(p) 102 | if prefixIdx < 0 || typeIdx < 0 { 103 | return ParseResult{}, fmt.Errorf("expected path with >=3 parts, got %q", p) 104 | } 105 | prefix := p[:prefixIdx] 106 | typStr := p[prefixIdx+1 : typeIdx] 107 | suffix := p[typeIdx+1:] 108 | 109 | typ := UnknownType 110 | switch Type(typStr) { 111 | case KeyType: 112 | typ = KeyType 113 | case CollectionType: 114 | typ = CollectionType 115 | case TransactionType: 116 | typ = TransactionType 117 | } 118 | 119 | return ParseResult{ 120 | Prefix: prefix, 121 | Suffix: suffix, 122 | Type: typ, 123 | }, nil 124 | } 125 | 126 | type ParseResult struct { 127 | Prefix string 128 | Suffix string 129 | Type Type 130 | } 131 | 132 | func pathPartsIndexes(p string) (prefixIdx int, typeIdx int) { 133 | typeIdx = strings.LastIndexByte(p, '/') 134 | if typeIdx <= 0 { 135 | return -1, -1 136 | } 137 | prefixIdx = strings.LastIndexByte(p[:typeIdx-1], '/') 138 | if typeIdx <= 0 { 139 | return -1, typeIdx 140 | } 141 | return prefixIdx, typeIdx 142 | } 143 | 144 | func typedPrefix(prefix string, t Type) string { 145 | buf := bufPool.Get().(*bytes.Buffer) 146 | buf.WriteString(prefix) 147 | buf.WriteByte('/') 148 | buf.WriteString(string(t)) 149 | buf.WriteByte('/') 150 | res := buf.String() 151 | buf.Reset() 152 | bufPool.Put(buf) 153 | return res 154 | } 155 | 156 | func prefixEncode(prefix string, category Type, a []byte) string { 157 | buf := bufPool.Get().(*bytes.Buffer) 158 | buf.WriteString(prefix) 159 | buf.WriteByte('/') 160 | encode(buf, category, a) 161 | res := buf.String() 162 | buf.Reset() 163 | bufPool.Put(buf) 164 | return res 165 | } 166 | 167 | func encode(buf *bytes.Buffer, category Type, a []byte) { 168 | buf.WriteString(string(category)) 169 | buf.WriteByte('/') 170 | buf.WriteString(encoding.EncodeToString(a)) 171 | } 172 | 173 | func decode(category Type, suffix string) ([]byte, error) { 174 | trimmed := strings.TrimPrefix(suffix, string(category)+"/") 175 | if trimmed == suffix { 176 | return nil, fmt.Errorf(`got path %q, expected prefix %q`, suffix, category) 177 | } 178 | return encoding.DecodeString(trimmed) 179 | } 180 | -------------------------------------------------------------------------------- /internal/data/txid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package data 16 | 17 | import ( 18 | "bytes" 19 | "crypto/rand" 20 | "encoding/hex" 21 | "fmt" 22 | ) 23 | 24 | // TxID represents a transaction id. 25 | type TxID []byte 26 | 27 | func (t TxID) String() string { 28 | return hex.EncodeToString(t) 29 | } 30 | 31 | func (t TxID) Equal(other TxID) bool { 32 | return bytes.Equal(t, other) 33 | } 34 | 35 | func NewTId() TxID { 36 | b := make([]byte, 16) 37 | if _, err := rand.Read(b); err != nil { 38 | panic(fmt.Sprintf("Cannot read from random device: %v", err)) 39 | } 40 | return b 41 | } 42 | 43 | func NewTxIDSet(ids ...TxID) TxIDSet { 44 | res := make(TxIDSet, 0, len(ids)) 45 | for _, id := range ids { 46 | res, _ = res.Add(id) 47 | } 48 | return res 49 | } 50 | 51 | // TxIDSet is a set of transaction IDs optimized for small sizes. 52 | type TxIDSet []TxID 53 | 54 | func (s TxIDSet) Add(a TxID) (TxIDSet, bool) { 55 | if s.Contains(a) { 56 | return s, false 57 | } 58 | return append(s, a), true 59 | } 60 | 61 | func (s TxIDSet) AddMulti(ids ...TxID) TxIDSet { 62 | res := s 63 | for _, e := range ids { 64 | res, _ = res.Add(e) 65 | } 66 | return res 67 | } 68 | 69 | func (s TxIDSet) Contains(a TxID) bool { 70 | for _, id := range s { 71 | if id.Equal(a) { 72 | return true 73 | } 74 | } 75 | return false 76 | } 77 | 78 | func (s TxIDSet) IndexOf(a TxID) int { 79 | for i, id := range s { 80 | if id.Equal(a) { 81 | return i 82 | } 83 | } 84 | return -1 85 | } 86 | 87 | // TxIDSetDiff is a set difference between a and b. 88 | // It returns a new set where all elements of b have been removed 89 | // from a. 90 | func TxIDSetDiff(a, b TxIDSet) TxIDSet { 91 | var res TxIDSet 92 | for _, candidate := range a { 93 | if !b.Contains(candidate) { 94 | // We can keep it. 95 | res = append(res, candidate) 96 | } 97 | } 98 | return res 99 | } 100 | 101 | // TxIDSetIntersect is a set intersection between a and b. 102 | // It returns a new set with only elements common between a and b. 103 | func TxIDSetIntersect(a, b TxIDSet) TxIDSet { 104 | var res TxIDSet 105 | for _, candidate := range a { 106 | if b.Contains(candidate) { 107 | // We can keep it. 108 | res = append(res, candidate) 109 | } 110 | } 111 | return res 112 | } 113 | 114 | // TxIDSetUnion returns a new set with the elements of both a and b. 115 | func TxIDSetUnion(a, b TxIDSet) TxIDSet { 116 | return NewTxIDSet(a...).AddMulti(b...) 117 | } 118 | -------------------------------------------------------------------------------- /internal/errors/err.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "sync" 23 | ) 24 | 25 | // Aliases to the standard errors package. 26 | var ( 27 | New = errors.New 28 | Is = errors.Is 29 | As = errors.As 30 | ) 31 | 32 | // bufferPool is a pool of bytes.Buffers. 33 | var bufferPool = sync.Pool{ 34 | New: func() interface{} { 35 | return &bytes.Buffer{} 36 | }, 37 | } 38 | 39 | // WithCause annotates a symptom error with a cause. 40 | // 41 | // Both errors can be discovered by the Is and As methods. 42 | func WithCause(symptom, cause error) error { 43 | return annotated{ 44 | cause: cause, 45 | symptom: symptom, 46 | } 47 | } 48 | 49 | func WithDetails(err error, details ...string) error { 50 | if err == nil { 51 | return nil 52 | } 53 | return detailed{err, details} 54 | } 55 | 56 | func WriteDetails(w io.Writer, err error) { 57 | var dErr detailed 58 | for errors.As(err, &dErr) { 59 | // Append all details of this error. 60 | iw := indentWriter{w} 61 | for _, d := range dErr.details { 62 | //nolint:errcheck 63 | io.WriteString(iw, "\n- ") 64 | //nolint:errcheck 65 | io.WriteString(indentWriter{iw}, d) 66 | } 67 | // Continue down the chain. 68 | err = dErr.error 69 | } 70 | } 71 | 72 | func Details(err error) string { 73 | buffer := bufferPool.Get().(*bytes.Buffer) 74 | WriteDetails(buffer, err) 75 | res := buffer.String() 76 | buffer.Reset() 77 | bufferPool.Put(buffer) 78 | return res 79 | } 80 | 81 | func Combine(errs ...error) error { 82 | if len(errs) == 0 { 83 | return nil 84 | } 85 | 86 | var ( 87 | res multi 88 | others []error 89 | ) 90 | for _, err := range errs { 91 | // Combine only the non-nil errors. 92 | if err == nil { 93 | continue 94 | } 95 | // Flatten by taking out the multi-error from the list 96 | // if any is present. 97 | merr, ok := err.(multi) //nolint:errorlint 98 | if ok { 99 | res = merr 100 | continue 101 | } 102 | others = append(others, err) 103 | } 104 | 105 | // If we found a multierror, return that and append the rest. 106 | if res != nil { 107 | return append(res, others...) 108 | } 109 | 110 | // Check the others. 111 | if len(others) == 0 { 112 | return nil 113 | } 114 | if len(others) == 1 { 115 | return others[0] 116 | } 117 | 118 | return multi(others) 119 | } 120 | 121 | func Errors(err error) []error { 122 | if err == nil { 123 | return nil 124 | } 125 | merr, ok := err.(multi) //nolint:errorlint 126 | if !ok { 127 | return []error{err} 128 | } 129 | return merr 130 | } 131 | 132 | type multi []error 133 | 134 | func (m multi) Error() string { 135 | return fmt.Sprintf("multiple errors (%d); sample: %v", len(m), m[0]) 136 | } 137 | 138 | func (m multi) Is(target error) bool { 139 | for _, err := range m { 140 | if errors.Is(err, target) { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | 147 | func (m multi) As(target interface{}) bool { 148 | for _, err := range m { 149 | if errors.As(err, target) { 150 | return true 151 | } 152 | } 153 | return false 154 | } 155 | 156 | func (m multi) Format(f fmt.State, c rune) { 157 | if (c == 'v' || c == 'w') && f.Flag('+') { 158 | m.writeMultiline(f) 159 | } else { 160 | //nolint:errcheck 161 | io.WriteString(f, m.Error()) 162 | } 163 | } 164 | 165 | func (m multi) writeMultiline(w io.Writer) { 166 | fmt.Fprintf(w, "multiple errors (%d):", len(m)) 167 | wi := indentWriter{w} 168 | for _, err := range m { 169 | //nolint:errcheck 170 | io.WriteString(w, "\n- ") 171 | //nolint:errcheck 172 | fmt.Fprintf(wi, "%+v", err) 173 | } 174 | } 175 | 176 | type detailed struct { 177 | error 178 | details []string 179 | } 180 | 181 | func (d detailed) Format(f fmt.State, c rune) { 182 | if (c == 'v' || c == 'w') && f.Flag('+') { 183 | d.writeMultiline(f) 184 | } else { 185 | //nolint:errcheck 186 | io.WriteString(f, d.Error()) 187 | } 188 | } 189 | 190 | func (d detailed) writeMultiline(w io.Writer) { 191 | fmt.Fprintf(w, "%+v", d.error) 192 | if len(d.details) == 0 { 193 | return 194 | } 195 | 196 | //nolint:errcheck 197 | io.WriteString(w, "\nNote:") 198 | iw := indentWriter{w} 199 | for _, d := range d.details { 200 | //nolint:errcheck 201 | io.WriteString(w, "\n- ") 202 | //nolint:errcheck 203 | io.WriteString(iw, d) 204 | } 205 | } 206 | 207 | type annotated struct { 208 | cause error 209 | symptom error 210 | } 211 | 212 | func (e annotated) Error() string { 213 | return fmt.Sprintf("%v: %v", e.cause, e.symptom) 214 | } 215 | 216 | func (e annotated) Unwrap() error { 217 | return e.cause 218 | } 219 | 220 | func (e annotated) Is(target error) bool { 221 | return errors.Is(e.symptom, target) || errors.Is(e.cause, target) 222 | } 223 | 224 | func (e annotated) As(target interface{}) bool { 225 | if errors.As(e.symptom, target) { 226 | return true 227 | } 228 | return errors.As(e.cause, target) 229 | } 230 | 231 | type indentWriter struct { 232 | io.Writer 233 | } 234 | 235 | func (w indentWriter) Write(b []byte) (int, error) { 236 | num := 0 237 | write := func(b []byte) error { 238 | n, err := w.Writer.Write(b) 239 | num += n 240 | return err 241 | } 242 | 243 | i, j := 0, 0 244 | for i < len(b) { 245 | if b[i] == '\n' { 246 | // Write the indentation. 247 | if err := write([]byte("\n ")); err != nil { 248 | return num, err 249 | } 250 | i++ 251 | } 252 | // Look for the next newline. 253 | for j = i; j < len(b) && b[j] != '\n'; j++ { 254 | } 255 | // Write everything up to the newline, or the end of buffer. 256 | if err := write(b[i:j]); err != nil { 257 | return num, err 258 | } 259 | i = j 260 | } 261 | 262 | return num, nil 263 | } 264 | -------------------------------------------------------------------------------- /internal/errors/err_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | type someError struct { 26 | a int 27 | } 28 | 29 | func (someError) Error() string { return "someError" } 30 | 31 | func TestAnnotated(t *testing.T) { 32 | err1 := errors.New("error1") 33 | err2 := someError{3} 34 | wrapped := WithCause(err2, err1) 35 | 36 | // The error is both err1 and err2 37 | assert.Equal(t, "error1: someError", wrapped.Error()) 38 | assert.True(t, errors.Is(wrapped, err1)) 39 | assert.True(t, errors.Is(wrapped, err2)) 40 | 41 | // Contents are preserved. 42 | var et someError 43 | assert.True(t, errors.As(wrapped, &et)) 44 | assert.Equal(t, err2, et) 45 | 46 | // Same properties with the errors inverted. 47 | wrapped = WithCause(err1, err2) 48 | assert.True(t, errors.Is(wrapped, err1)) 49 | assert.True(t, errors.Is(wrapped, err2)) 50 | assert.True(t, errors.As(wrapped, &et)) 51 | assert.Equal(t, err2, et) 52 | } 53 | 54 | func TestErrWithDetails(t *testing.T) { 55 | err1 := errors.New("err1") 56 | err2 := WithDetails( 57 | fmt.Errorf("err2: %w", err1), 58 | "second descr\nmultiline", 59 | "another\ndescr", 60 | ) 61 | err3 := WithDetails( 62 | fmt.Errorf("err3: %w", err2), 63 | "third descr\nmultiline\nmultiline again") 64 | err4 := fmt.Errorf("err4: %w", err3) 65 | assert.Equal(t, "err4: err3: err2: err1", fmt.Sprintf("%v", err4)) 66 | details := ` 67 | - third descr 68 | multiline 69 | multiline again 70 | - second descr 71 | multiline 72 | - another 73 | descr` 74 | assert.Equal(t, details, Details(err4)) 75 | } 76 | 77 | func TestCombine(t *testing.T) { 78 | err1 := errors.New("err1") 79 | err2 := errors.New("err2") 80 | 81 | // Combine should ignore nils. 82 | assert.NoError(t, Combine()) 83 | assert.NoError(t, Combine(nil)) 84 | assert.NoError(t, Combine(nil, nil)) 85 | assert.EqualError(t, Combine(err1, nil), err1.Error()) 86 | 87 | // Combine two. 88 | assert.ElementsMatch(t, Errors(Combine(err1, err2)), []error{err1, err2}) 89 | assert.ElementsMatch(t, Errors(Combine(err1, nil, err2)), []error{err1, err2}) 90 | 91 | // Nesting. 92 | err3 := Combine(err1, err2) 93 | err4 := Combine(err1, err2, err3, nil) 94 | assert.ElementsMatch(t, Errors(err4), []error{err1, err1, err2, err2}) 95 | 96 | // Print. 97 | assert.EqualError(t, err4, "multiple errors (4); sample: err1") 98 | verbose := `multiple errors (4): 99 | - err1 100 | - err2 101 | - err1 102 | - err2` 103 | assert.Equal(t, fmt.Sprintf("%+v", err4), verbose) 104 | } 105 | -------------------------------------------------------------------------------- /internal/proto/proto.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package proto 16 | 17 | //go:generate protoc -I=. --go_out=. --go_opt=paths=source_relative transaction.proto 18 | -------------------------------------------------------------------------------- /internal/proto/transaction.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package glassdb; 18 | 19 | option go_package = "github.com/mbrt/glassdb/internal/proto"; 20 | 21 | import "google/protobuf/timestamp.proto"; 22 | 23 | message TransactionLog { 24 | enum Status { 25 | DEFAULT = 0; 26 | COMMITTED = 1; 27 | ABORTED = 2; 28 | PENDING = 3; 29 | } 30 | 31 | google.protobuf.Timestamp timestamp = 1; 32 | Status status = 2; 33 | repeated CollectionWrites writes = 3; 34 | } 35 | 36 | message CollectionWrites { 37 | string prefix = 1; 38 | repeated Write writes = 2; 39 | CollectionLocks locks = 3; 40 | } 41 | 42 | message Write { 43 | string suffix = 1; 44 | // The ID of the last transaction writing to this key. 45 | bytes prev_tid = 3; 46 | 47 | oneof val_delete { 48 | bytes value = 2; 49 | bool deleted = 4; 50 | } 51 | } 52 | 53 | message CollectionLocks { 54 | Lock.LockType collection_lock = 2; 55 | repeated Lock locks = 3; 56 | } 57 | 58 | message Lock { 59 | enum LockType { 60 | UNKNOWN = 0; 61 | NONE = 1; 62 | READ = 2; 63 | WRITE = 3; 64 | CREATE = 4; 65 | } 66 | 67 | string suffix = 1; 68 | LockType lock_type = 2; 69 | } 70 | -------------------------------------------------------------------------------- /internal/storage/global.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/jonboulle/clockwork" 22 | 23 | "github.com/mbrt/glassdb/backend" 24 | "github.com/mbrt/glassdb/internal/errors" 25 | ) 26 | 27 | func NewGlobal(b backend.Backend, l Local, clock clockwork.Clock) Global { 28 | return Global{ 29 | backend: b, 30 | local: l, 31 | clock: clock, 32 | } 33 | } 34 | 35 | type Global struct { 36 | backend backend.Backend 37 | local Local 38 | clock clockwork.Clock 39 | } 40 | 41 | func (s Global) Read(ctx context.Context, key string) (GlobalRead, error) { 42 | // If we have the object in local storage, read it only if it was modified. 43 | // Otherwise read without optimizations. 44 | if e, ok := s.local.Read(key, MaxStaleness); ok { 45 | // If this is a local override, or we are 100% sure the value is 46 | // outdated, it's better to do a regular read. 47 | if !e.Outdated && !e.Version.B.IsNull() { 48 | modified := true 49 | r, err := s.backend.ReadIfModified(ctx, key, e.Version.B.Contents) 50 | if err != nil { 51 | if !errors.Is(err, backend.ErrPrecondition) { 52 | return GlobalRead{}, fmt.Errorf("backend read of %q: %w", key, err) 53 | } 54 | modified = false 55 | } 56 | if modified { 57 | // The local value was stale. Take the updated value from the backend. 58 | // Update local storage. 59 | s.local.Write(key, r.Contents, Version{B: r.Version}) 60 | 61 | return GlobalRead{ 62 | Value: r.Contents, 63 | Version: r.Version.Contents, 64 | }, nil 65 | } 66 | // The cached value is up to date, use that. 67 | return GlobalRead{ 68 | Value: e.Value, 69 | Version: e.Version.B.Contents, 70 | }, nil 71 | } 72 | } 73 | 74 | // No value in cache. Read directly. 75 | r, err := s.backend.Read(ctx, key) 76 | if err != nil { 77 | return GlobalRead{}, fmt.Errorf("backend read of %q: %w", key, err) 78 | } 79 | // Update local storage. 80 | s.local.Write(key, r.Contents, Version{B: r.Version}) 81 | 82 | return GlobalRead{ 83 | Value: r.Contents, 84 | Version: r.Version.Contents, 85 | }, nil 86 | } 87 | 88 | func (s Global) GetMetadata(ctx context.Context, key string) (backend.Metadata, error) { 89 | meta, err := s.backend.GetMetadata(ctx, key) 90 | if err != nil { 91 | return backend.Metadata{}, err 92 | } 93 | s.local.SetMeta(key, meta) 94 | return meta, nil 95 | } 96 | 97 | func (s Global) SetTagsIf( 98 | ctx context.Context, 99 | key string, 100 | expected backend.Version, 101 | t backend.Tags, 102 | ) (backend.Metadata, error) { 103 | meta, err := s.backend.SetTagsIf(ctx, key, expected, t) 104 | if err != nil { 105 | return backend.Metadata{}, err 106 | } 107 | s.local.SetMeta(key, meta) 108 | return meta, nil 109 | } 110 | 111 | func (s Global) Write( 112 | ctx context.Context, 113 | key string, 114 | value []byte, 115 | t backend.Tags, 116 | ) (backend.Metadata, error) { 117 | meta, err := s.backend.Write(ctx, key, value, t) 118 | if err != nil { 119 | return meta, err 120 | } 121 | s.local.WriteWithMeta(key, value, meta) 122 | return meta, nil 123 | } 124 | 125 | func (s Global) WriteIf( 126 | ctx context.Context, 127 | key string, 128 | value []byte, 129 | expected backend.Version, 130 | t backend.Tags, 131 | ) (backend.Metadata, error) { 132 | meta, err := s.backend.WriteIf(ctx, key, value, expected, t) 133 | if err != nil { 134 | return backend.Metadata{}, err 135 | } 136 | s.local.WriteWithMeta(key, value, meta) 137 | return meta, nil 138 | } 139 | 140 | func (s Global) WriteIfNotExists( 141 | ctx context.Context, 142 | key string, 143 | value []byte, 144 | t backend.Tags, 145 | ) (backend.Metadata, error) { 146 | meta, err := s.backend.WriteIfNotExists(ctx, key, value, t) 147 | if err != nil { 148 | return meta, err 149 | } 150 | s.local.WriteWithMeta(key, value, meta) 151 | return meta, nil 152 | } 153 | 154 | func (s Global) Delete(ctx context.Context, key string) error { 155 | if err := s.backend.Delete(ctx, key); err != nil { 156 | return err 157 | } 158 | s.local.Delete(key) 159 | return nil 160 | } 161 | 162 | func (s Global) DeleteIf(ctx context.Context, key string, expected backend.Version) error { 163 | if err := s.backend.DeleteIf(ctx, key, expected); err != nil { 164 | return err 165 | } 166 | s.local.Delete(key) 167 | return nil 168 | } 169 | 170 | type GlobalRead struct { 171 | Value []byte 172 | Version int64 173 | } 174 | -------------------------------------------------------------------------------- /internal/storage/local.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "math" 19 | "time" 20 | 21 | "github.com/jonboulle/clockwork" 22 | 23 | "github.com/mbrt/glassdb/backend" 24 | "github.com/mbrt/glassdb/internal/cache" 25 | ) 26 | 27 | const MaxStaleness = time.Duration(math.MaxInt64) 28 | 29 | func NewLocal(c *cache.Cache, clock clockwork.Clock) Local { 30 | return Local{c, clock} 31 | } 32 | 33 | type Local struct { 34 | cache *cache.Cache 35 | clock clockwork.Clock 36 | } 37 | 38 | func (c Local) Read(key string, maxStale time.Duration) (LocalRead, bool) { 39 | e, ok := c.entry(key) 40 | if !ok || e.V == nil { 41 | return LocalRead{}, false 42 | } 43 | if c.isStale(e.V.Updated, maxStale) { 44 | return LocalRead{}, false 45 | } 46 | 47 | return LocalRead{ 48 | Value: e.V.Value, 49 | Version: e.V.Version, 50 | Deleted: e.V.Deleted, 51 | Outdated: e.isValueOutdated(), 52 | }, true 53 | } 54 | 55 | func (c Local) GetMeta(key string, maxStale time.Duration) (LocalMetadata, bool) { 56 | e, ok := c.entry(key) 57 | if !ok || e.M == nil { 58 | return LocalMetadata{}, false 59 | } 60 | if c.isStale(e.M.Updated, maxStale) { 61 | // The value is too stale. 62 | return LocalMetadata{}, false 63 | } 64 | return LocalMetadata{ 65 | M: e.M.Meta, 66 | Outdated: e.isMetaOutdated(), 67 | }, true 68 | } 69 | 70 | func (c Local) WriteWithMeta(key string, value []byte, meta backend.Metadata) { 71 | updated := c.clock.Now() 72 | newEntry := cacheEntry{ 73 | V: &cacheValue{ 74 | Value: value, 75 | Version: VersionFromMeta(meta), 76 | Updated: updated, 77 | }, 78 | M: &cacheMeta{ 79 | Meta: meta, 80 | Updated: updated, 81 | }, 82 | } 83 | c.cache.Set(key, newEntry) 84 | } 85 | 86 | func (c Local) Write(key string, value []byte, v Version) { 87 | newValue := &cacheValue{ 88 | Value: value, 89 | Version: v, 90 | Updated: c.clock.Now(), 91 | } 92 | c.cache.Update(key, func(v cache.Value) cache.Value { 93 | if v == nil { 94 | return cacheEntry{V: newValue} 95 | } 96 | entry := v.(cacheEntry) 97 | entry.V = newValue 98 | return entry 99 | }) 100 | } 101 | 102 | func (c Local) SetMeta(key string, meta backend.Metadata) { 103 | newMeta := &cacheMeta{ 104 | Meta: meta, 105 | Updated: c.clock.Now(), 106 | } 107 | 108 | c.cache.Update(key, func(v cache.Value) cache.Value { 109 | if v == nil { 110 | return cacheEntry{M: newMeta} 111 | } 112 | entry := v.(cacheEntry) 113 | entry.M = newMeta 114 | return entry 115 | }) 116 | } 117 | 118 | // MarkStale annotates the given key value as outdated, only if it's currently 119 | // set at the given version. 120 | func (c Local) MarkValueOutated(key string, v Version) { 121 | c.cache.Update(key, func(old cache.Value) cache.Value { 122 | if old == nil { 123 | // Nothing to do. The value is not there anymore. 124 | return nil 125 | } 126 | entry := old.(cacheEntry) 127 | // Mark as outdated only if the version is the same. 128 | if entry.V != nil && entry.V.Version.EqualContents(v) { 129 | // Do not change the value directly to avoid race conditions. 130 | // Return a copy instead. 131 | newval := *entry.V 132 | newval.Outdated = true 133 | entry = cacheEntry{ 134 | V: &newval, 135 | M: entry.M, 136 | } 137 | } 138 | return entry 139 | }) 140 | } 141 | 142 | func (c Local) MarkDeleted(key string, v Version) { 143 | newValue := &cacheValue{ 144 | Deleted: true, 145 | Version: v, 146 | Updated: c.clock.Now(), 147 | } 148 | c.cache.Update(key, func(v cache.Value) cache.Value { 149 | if v == nil { 150 | return cacheEntry{V: newValue} 151 | } 152 | entry := v.(cacheEntry) 153 | entry.V = newValue 154 | return entry 155 | }) 156 | } 157 | 158 | func (c Local) Delete(key string) { 159 | c.cache.Delete(key) 160 | } 161 | 162 | func (c Local) entry(key string) (cacheEntry, bool) { 163 | e, ok := c.cache.Get(key) 164 | if !ok { 165 | return cacheEntry{}, false 166 | } 167 | return e.(cacheEntry), true 168 | } 169 | 170 | func (c Local) isStale(updated time.Time, maxStaleness time.Duration) bool { 171 | staleness := c.clock.Now().Sub(updated) 172 | return staleness > maxStaleness 173 | } 174 | 175 | type LocalRead struct { 176 | Value []byte 177 | Version Version 178 | Deleted bool 179 | // Outdated is true if the value read is certainly outdated. 180 | Outdated bool 181 | } 182 | 183 | type LocalMetadata struct { 184 | M backend.Metadata 185 | // Outdated is true if the metadata is certainly outdated. 186 | Outdated bool 187 | } 188 | 189 | type cacheEntry struct { 190 | V *cacheValue 191 | M *cacheMeta 192 | } 193 | 194 | func (c cacheEntry) SizeB() int { 195 | var res int 196 | if c.V != nil { 197 | res += len(c.V.Value) + len(c.V.Version.Writer) 198 | } 199 | if c.M != nil { 200 | // Estimate 16 bytes per tag. 201 | res += len(c.M.Meta.Tags) * 16 202 | } 203 | return res 204 | } 205 | 206 | func (c cacheEntry) isMetaOutdated() bool { 207 | // Assume c.M is non nil. 208 | if c.V == nil || c.V.Version.B.IsNull() { 209 | return false 210 | } 211 | valVersion := c.V.Version.B 212 | metaVersion := c.M.Meta.Version 213 | if valVersion == metaVersion { 214 | return false 215 | } 216 | // The versions are different. If the value was updated last, 217 | // then metadata is definitely outdated. 218 | return c.V.Updated.After(c.M.Updated) 219 | } 220 | 221 | func (c cacheEntry) isValueOutdated() bool { 222 | if c.V.Outdated { 223 | // The value is outdated for sure. 224 | return true 225 | } 226 | // Assume c.V is non nil. 227 | if c.M == nil { 228 | return false 229 | } 230 | valVersion := c.V.Version.B 231 | metaVersion := c.M.Meta.Version 232 | if valVersion.IsNull() { 233 | // We have a local override (only version.Writer != nil). 234 | // We don't know whether it's outdated. 235 | return false 236 | } 237 | if valVersion.Contents == metaVersion.Contents { 238 | return false 239 | } 240 | // The versions are different. If metadata was updated last, 241 | // then the value is definitely outdated. 242 | return c.M.Updated.After(c.V.Updated) 243 | } 244 | 245 | type cacheValue struct { 246 | Value []byte 247 | Deleted bool 248 | // Marks the given value as outdated for sure. 249 | // If false we don't know. 250 | Outdated bool 251 | Version Version 252 | Updated time.Time 253 | } 254 | 255 | type cacheMeta struct { 256 | Meta backend.Metadata 257 | Updated time.Time 258 | } 259 | -------------------------------------------------------------------------------- /internal/storage/tlogger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "github.com/jonboulle/clockwork" 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | 26 | "github.com/mbrt/glassdb/backend" 27 | "github.com/mbrt/glassdb/backend/memory" 28 | "github.com/mbrt/glassdb/internal/cache" 29 | "github.com/mbrt/glassdb/internal/data" 30 | "github.com/mbrt/glassdb/internal/data/paths" 31 | "github.com/mbrt/glassdb/internal/testkit" 32 | ) 33 | 34 | const ( 35 | testCollName = "testp" 36 | clockMultiplier = 1000 37 | ) 38 | 39 | var collInfoContents = []byte("__foo__") 40 | 41 | type testContext struct { 42 | backend backend.Backend 43 | clock clockwork.Clock 44 | global Global 45 | local Local 46 | } 47 | 48 | func testTLoggerContext(t *testing.T) (TLogger, testContext) { 49 | t.Helper() 50 | return newTLoggerFromBackend(t, memory.New()) 51 | } 52 | 53 | func newTLoggerFromBackend(t *testing.T, b backend.Backend) (TLogger, testContext) { 54 | t.Helper() 55 | 56 | clock := testkit.NewAcceleratedClock(clockMultiplier) 57 | cache := cache.New(1024) 58 | local := NewLocal(cache, clock) 59 | global := NewGlobal(b, local, clock) 60 | tlogger := NewTLogger(clock, global, local, testCollName) 61 | 62 | // Create test collection. 63 | ctx := context.Background() 64 | _, err := global.Write(ctx, paths.CollectionInfo(testCollName), collInfoContents, nil) 65 | require.NoError(t, err) 66 | 67 | return tlogger, testContext{ 68 | backend: b, 69 | clock: clock, 70 | global: global, 71 | local: local, 72 | } 73 | } 74 | 75 | func TestGetSet(t *testing.T) { 76 | tl, tctx := testTLoggerContext(t) 77 | ctx := context.Background() 78 | tx := data.TxID([]byte("tx1")) 79 | 80 | // Unknown transactions are not found. 81 | _, err := tl.Get(ctx, tx) 82 | assert.ErrorIs(t, err, backend.ErrNotFound) 83 | // Their status is unknown. 84 | status, err := tl.CommitStatus(ctx, tx) 85 | assert.NoError(t, err) 86 | assert.Equal(t, TxCommitStatusUnknown, status.Status) 87 | 88 | // We don't handle sub-second precision from within TLogger. 89 | beforeSetTs := tctx.clock.Now().Truncate(time.Second) 90 | collection := paths.FromCollection("example", []byte("coll")) 91 | 92 | log := TxLog{ 93 | ID: tx, 94 | Status: TxCommitStatusOK, 95 | Writes: []TxWrite{ 96 | { 97 | Path: paths.FromKey(collection, []byte("key1")), 98 | Value: []byte("val"), 99 | }, 100 | }, 101 | Locks: []PathLock{ 102 | { 103 | Path: paths.CollectionInfo(collection), 104 | Type: LockTypeWrite, 105 | }, 106 | { 107 | Path: paths.FromKey(collection, []byte("key1")), 108 | Type: LockTypeCreate, 109 | }, 110 | { 111 | Path: paths.FromKey(collection, []byte("key2")), 112 | Type: LockTypeUnknown, 113 | }, 114 | { 115 | Path: paths.CollectionInfo(paths.FromCollection("example", []byte("coll2"))), 116 | Type: LockTypeRead, 117 | }, 118 | }, 119 | } 120 | 121 | // Write the log. 122 | _, err = tl.Set(ctx, log) 123 | assert.NoError(t, err) 124 | 125 | // The log comes back. 126 | gotlog, err := tl.Get(ctx, tx) 127 | assert.NoError(t, err) 128 | assert.Equal(t, log.ID, gotlog.ID) 129 | assert.Equal(t, log.Status, gotlog.Status) 130 | assert.GreaterOrEqual(t, gotlog.Timestamp, beforeSetTs) 131 | assert.ElementsMatch(t, log.Locks, gotlog.Locks) 132 | assert.ElementsMatch(t, log.Writes, gotlog.Writes) 133 | 134 | // The status is correct. 135 | status, err = tl.CommitStatus(ctx, tx) 136 | assert.NoError(t, err) 137 | assert.Equal(t, TxCommitStatusOK, status.Status) 138 | assert.GreaterOrEqual(t, status.LastUpdate, beforeSetTs) 139 | } 140 | 141 | func TestPendingUpdate(t *testing.T) { 142 | tl1, tctx := testTLoggerContext(t) 143 | tl2, _ := newTLoggerFromBackend(t, tctx.backend) 144 | ctx := context.Background() 145 | tx := data.TxID([]byte("tx1")) 146 | 147 | // We don't handle sub-second precision from within TLogger. 148 | beforeSetTs := tctx.clock.Now().Truncate(time.Second) 149 | 150 | vers, err := tl1.Set(ctx, TxLog{ 151 | ID: tx, 152 | Status: TxCommitStatusPending, 153 | }) 154 | assert.NoError(t, err) 155 | 156 | // Check status and get. 157 | status, err := tl2.CommitStatus(ctx, tx) 158 | assert.NoError(t, err) 159 | assert.Equal(t, TxCommitStatusPending, status.Status) 160 | assert.GreaterOrEqual(t, status.LastUpdate, beforeSetTs) 161 | log, err := tl2.Get(ctx, tx) 162 | assert.NoError(t, err) 163 | assert.Equal(t, tx, log.ID) 164 | assert.Equal(t, TxCommitStatusPending, log.Status) 165 | assert.GreaterOrEqual(t, log.Timestamp, beforeSetTs) 166 | 167 | // Wait some time. 168 | tctx.clock.Sleep(5 * time.Second) 169 | beforeOverwriteTs := tctx.clock.Now().Truncate(time.Second) 170 | 171 | // Overwrite the log with an updated pending state. 172 | _, err = tl1.SetIf(ctx, TxLog{ 173 | ID: tx, 174 | Status: TxCommitStatusPending, 175 | }, vers) 176 | assert.NoError(t, err) 177 | 178 | // We should see the updated timestamp. 179 | status, err = tl2.CommitStatus(ctx, tx) 180 | assert.NoError(t, err) 181 | assert.GreaterOrEqual(t, status.LastUpdate, beforeOverwriteTs) 182 | log, err = tl2.Get(ctx, tx) 183 | assert.NoError(t, err) 184 | assert.GreaterOrEqual(t, log.Timestamp, beforeOverwriteTs) 185 | } 186 | -------------------------------------------------------------------------------- /internal/storage/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "github.com/mbrt/glassdb/backend" 19 | "github.com/mbrt/glassdb/internal/data" 20 | ) 21 | 22 | type Version struct { 23 | B backend.Version 24 | Writer data.TxID 25 | } 26 | 27 | func (v Version) IsNull() bool { 28 | return v.B.IsNull() && v.Writer == nil 29 | } 30 | 31 | func (v Version) IsLocal() bool { 32 | return v.Writer != nil 33 | } 34 | 35 | func (v Version) EqualContents(other Version) bool { 36 | if !v.B.IsNull() { 37 | return v.B.Contents == other.B.Contents 38 | } 39 | return v.Writer.Equal(other.Writer) 40 | } 41 | 42 | func (v Version) EqualMetaContents(m backend.Metadata) bool { 43 | if !v.B.IsNull() { 44 | return v.B.Contents == m.Version.Contents 45 | } 46 | writer, _ := lastWriterFromTags(m.Tags) 47 | return writer != nil && v.Writer.Equal(writer) 48 | } 49 | 50 | func VersionFromMeta(m backend.Metadata) Version { 51 | writer, _ := lastWriterFromTags(m.Tags) 52 | return Version{ 53 | B: m.Version, 54 | Writer: writer, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/stringset/stringset.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package stringset helps with common set operations on strings. 16 | // 17 | // This is inspired by https://github.com/uber/kraken/blob/master/utils/stringset/stringset.go. 18 | package stringset 19 | 20 | // Set is a nifty little wrapper for common set operations on a map. Because it 21 | // is equivalent to a map, make/range/len will still work with Set. 22 | type Set map[string]struct{} 23 | 24 | // New creates a new Set with xs. 25 | func New(xs ...string) Set { 26 | s := make(Set) 27 | for _, x := range xs { 28 | s.Add(x) 29 | } 30 | return s 31 | } 32 | 33 | // Add adds x to s. 34 | func (s Set) Add(x string) { 35 | s[x] = struct{}{} 36 | } 37 | 38 | // Remove removes x from s. 39 | func (s Set) Remove(x string) { 40 | delete(s, x) 41 | } 42 | 43 | // Has returns true if x is in s. 44 | func (s Set) Has(x string) bool { 45 | _, ok := s[x] 46 | return ok 47 | } 48 | 49 | // ToSlice converts s to a slice. 50 | func (s Set) ToSlice() []string { 51 | var xs []string 52 | for x := range s { 53 | xs = append(xs, x) 54 | } 55 | return xs 56 | } 57 | -------------------------------------------------------------------------------- /internal/testkit/bench/bench.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package bench 16 | 17 | import ( 18 | "math" 19 | "sort" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | const ( 25 | defaultDuration = 10 * time.Second 26 | minSamples = 10 27 | ) 28 | 29 | type Bench struct { 30 | startTime time.Time 31 | totDuration time.Duration 32 | expectedDuration time.Duration 33 | samples []time.Duration 34 | m sync.Mutex 35 | } 36 | 37 | func (b *Bench) SetDuration(d time.Duration) { 38 | b.expectedDuration = d 39 | } 40 | 41 | func (b *Bench) Start() { 42 | b.startTime = time.Now() 43 | if b.expectedDuration == 0 { 44 | b.expectedDuration = defaultDuration 45 | } 46 | } 47 | 48 | func (b *Bench) End() { 49 | b.totDuration = time.Since(b.startTime) 50 | } 51 | 52 | func (b *Bench) IsTestFinished() bool { 53 | return time.Since(b.startTime) >= b.expectedDuration && len(b.samples) >= minSamples 54 | } 55 | 56 | func (b *Bench) Measure(fn func() error) error { 57 | start := time.Now() 58 | if err := fn(); err != nil { 59 | return err 60 | } 61 | d := time.Since(start) 62 | 63 | b.m.Lock() 64 | b.samples = append(b.samples, d) 65 | b.m.Unlock() 66 | return nil 67 | } 68 | 69 | func (b *Bench) Results() Results { 70 | b.m.Lock() 71 | res := Results{ 72 | Samples: make([]time.Duration, len(b.samples)), 73 | TotDuration: b.totDuration, 74 | } 75 | copy(res.Samples, b.samples) 76 | b.m.Unlock() 77 | return res 78 | } 79 | 80 | type Results struct { 81 | Samples []time.Duration 82 | TotDuration time.Duration 83 | } 84 | 85 | func (r Results) Avg() time.Duration { 86 | sum := time.Duration(0) 87 | for _, t := range r.Samples { 88 | sum += t 89 | } 90 | return time.Duration(float64(sum) / float64(len(r.Samples))) 91 | } 92 | 93 | func (r Results) Percentile(pctile float64) time.Duration { 94 | if len(r.Samples) == 0 || pctile < 0 || pctile > 1 { 95 | panic("invalid parameters") 96 | } 97 | xs := make([]float64, len(r.Samples)) 98 | for i := 0; i < len(r.Samples); i++ { 99 | xs[i] = float64(r.Samples[i]) 100 | } 101 | return time.Duration(percentile(xs, pctile)) 102 | } 103 | 104 | func percentile(sx []float64, pctile float64) float64 { 105 | // Interpolation method R8 from Hyndman and Fan (1996). 106 | sort.Float64Slice(sx).Sort() 107 | N := float64(len(sx)) 108 | // n := pctile * (N + 1) // R6 109 | n := 1/3.0 + pctile*(N+1/3.0) // R8 110 | kf, frac := math.Modf(n) 111 | k := int(kf) 112 | if k <= 0 { 113 | return sx[0] 114 | } else if k >= len(sx) { 115 | return sx[len(sx)-1] 116 | } 117 | return sx[k-1] + frac*(sx[k]-sx[k-1]) 118 | } 119 | -------------------------------------------------------------------------------- /internal/testkit/clock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "github.com/jonboulle/clockwork" 23 | ) 24 | 25 | func NewSelfAdvanceClock(t *testing.T) clockwork.Clock { 26 | c := clockwork.NewFakeClock() 27 | ctx, cancel := context.WithCancel(context.Background()) 28 | t.Cleanup(cancel) 29 | 30 | go func() { 31 | for { 32 | if err := ctx.Err(); err != nil { 33 | return 34 | } 35 | c.BlockUntil(1) 36 | c.Advance(100 * time.Millisecond) 37 | // Wait some real time before advancing again. 38 | // This allows sleepers to start running. 39 | time.Sleep(100 * time.Microsecond) 40 | } 41 | }() 42 | 43 | return c 44 | } 45 | 46 | func NewAcceleratedClock(multiplier int) clockwork.Clock { 47 | c := clockwork.NewRealClock() 48 | return aclock{ 49 | inner: c, 50 | multiplier: multiplier, 51 | epoch: c.Now(), 52 | } 53 | } 54 | 55 | type aclock struct { 56 | inner clockwork.Clock 57 | multiplier int 58 | epoch time.Time 59 | } 60 | 61 | func (c aclock) After(d time.Duration) <-chan time.Time { 62 | return c.inner.After(c.compress(d)) 63 | } 64 | 65 | func (c aclock) Sleep(d time.Duration) { 66 | c.inner.Sleep(c.compress(d)) 67 | } 68 | 69 | func (c aclock) Now() time.Time { 70 | since := c.inner.Since(c.epoch) 71 | return c.epoch.Add(c.expand(since)) 72 | } 73 | 74 | func (c aclock) Since(t time.Time) time.Duration { 75 | return c.Now().Sub(t) 76 | } 77 | 78 | func (c aclock) Until(t time.Time) time.Duration { 79 | return t.Sub(c.Now()) 80 | } 81 | 82 | func (c aclock) NewTicker(d time.Duration) clockwork.Ticker { 83 | return c.inner.NewTicker(c.compress(d)) 84 | } 85 | 86 | func (c aclock) NewTimer(d time.Duration) clockwork.Timer { 87 | return atimer{ 88 | Timer: c.inner.NewTimer(c.compress(d)), 89 | multiplier: c.multiplier, 90 | } 91 | } 92 | 93 | func (c aclock) AfterFunc(d time.Duration, fn func()) clockwork.Timer { 94 | d = c.compress(d) 95 | t := c.inner.AfterFunc(d, fn) 96 | return atimer{ 97 | Timer: t, 98 | multiplier: c.multiplier, 99 | } 100 | } 101 | 102 | func (c aclock) compress(d time.Duration) time.Duration { 103 | return d / time.Duration(c.multiplier) 104 | } 105 | 106 | func (c aclock) expand(d time.Duration) time.Duration { 107 | return d * time.Duration(c.multiplier) 108 | } 109 | 110 | type atimer struct { 111 | clockwork.Timer 112 | multiplier int 113 | } 114 | 115 | func (t atimer) Reset(d time.Duration) bool { 116 | return t.Timer.Reset(d / time.Duration(t.multiplier)) 117 | } 118 | 119 | // Make sure the Clock interface is implemented correctly. 120 | var _ clockwork.Clock = (*aclock)(nil) 121 | -------------------------------------------------------------------------------- /internal/testkit/clock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | func TestAcceleratedClock(t *testing.T) { 25 | c := NewAcceleratedClock(100) 26 | startA := c.Now() 27 | startR := time.Now() 28 | c.Sleep(time.Second) 29 | assertAround(t, 10*time.Millisecond, time.Since(startR)) 30 | assertAround(t, time.Second, c.Since(startA)) 31 | } 32 | 33 | func TestAcceleratedTimer(t *testing.T) { 34 | c := NewAcceleratedClock(100) 35 | startA := c.Now() 36 | startR := time.Now() 37 | timer := c.NewTimer(time.Second) 38 | defer timer.Stop() 39 | 40 | ch := timer.Chan() 41 | 42 | for i := 0; i < 10; i++ { 43 | <-ch 44 | assertAround(t, 10*time.Millisecond, time.Since(startR)) 45 | assertAround(t, time.Second, c.Since(startA)) 46 | 47 | timer.Reset(time.Second) 48 | startA = c.Now() 49 | startR = time.Now() 50 | } 51 | } 52 | 53 | func assertAround(t *testing.T, expected, got time.Duration) { 54 | t.Helper() 55 | assert.Less(t, got, expected*4) 56 | assert.Greater(t, got, expected/4) 57 | } 58 | -------------------------------------------------------------------------------- /internal/testkit/fake_gcs_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | "testing" 24 | 25 | "cloud.google.com/go/storage" 26 | "google.golang.org/api/option" 27 | ) 28 | 29 | func NewGCSClient(ctx context.Context, t testing.TB, logging bool) *storage.Client { 30 | t.Helper() 31 | 32 | srv := newFakeGCSServer() 33 | t.Cleanup(srv.Close) 34 | 35 | hclient := srv.Client() 36 | if logging { 37 | hclient.Transport = logTransport{hclient.Transport} 38 | } 39 | 40 | client, err := storage.NewClient(ctx, 41 | option.WithoutAuthentication(), 42 | option.WithEndpoint(srv.URL), 43 | option.WithHTTPClient(hclient), 44 | ) 45 | if err != nil { 46 | t.Fatalf("creating fake client: %v", err) 47 | } 48 | t.Cleanup(func() { 49 | client.Close() 50 | }) 51 | 52 | return client 53 | } 54 | 55 | type logTransport struct { 56 | inner http.RoundTripper 57 | } 58 | 59 | func (l logTransport) RoundTrip(req *http.Request) (*http.Response, error) { 60 | log1 := l.logRequest(req) 61 | resp, err := l.inner.RoundTrip(req) 62 | log2 := l.logResponse(resp) 63 | fmt.Printf("- request: %s\n response: %s\n", log1, log2) 64 | return resp, err 65 | } 66 | 67 | func (l logTransport) logRequest(req *http.Request) string { 68 | var buf bytes.Buffer 69 | 70 | // Log headers and form. 71 | _ = req.ParseForm() 72 | fmt.Fprintf(&buf, "method=%s path=%s headers=%v form=%v", 73 | req.Method, req.URL.Path, req.Header, req.Form) 74 | 75 | // Log body. 76 | if req.Body != nil { 77 | defer req.Body.Close() 78 | buf.WriteString(" body=") 79 | req.Body = copyAndRewind(req.Body, &buf) 80 | } 81 | 82 | return buf.String() 83 | } 84 | 85 | func (l logTransport) logResponse(r *http.Response) string { 86 | if r == nil { 87 | return "nil" 88 | } 89 | 90 | var buf bytes.Buffer 91 | 92 | fmt.Fprintf(&buf, "status=%s headers=%v", r.Status, r.Header) 93 | 94 | if r.Body != nil { 95 | defer r.Body.Close() 96 | buf.WriteString(" body=") 97 | r.Body = copyAndRewind(r.Body, &buf) 98 | } 99 | 100 | return buf.String() 101 | } 102 | 103 | func copyAndRewind(r io.Reader, w io.Writer) io.ReadCloser { 104 | var buf bytes.Buffer 105 | _, err := io.Copy(&buf, r) 106 | _, _ = w.Write(buf.Bytes()) 107 | return dummyCloser{&buf, err} 108 | } 109 | 110 | type dummyCloser struct { 111 | *bytes.Buffer 112 | err error 113 | } 114 | 115 | func (d dummyCloser) Close() error { 116 | return d.err 117 | } 118 | -------------------------------------------------------------------------------- /internal/testkit/fake_gcs_server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "net/http" 21 | "net/http/httptest" 22 | "strconv" 23 | 24 | "github.com/gorilla/mux" 25 | ) 26 | 27 | var errNotImplemented = withStatus( 28 | errors.New("not implemented"), http.StatusNotImplemented) 29 | 30 | func newFakeGCSServer() *httptest.Server { 31 | srv := &fakeGCSServer{gcs: newFakeGCS()} 32 | mux := srv.buildMux() 33 | return httptest.NewServer(mux) 34 | } 35 | 36 | type fakeGCSServer struct { 37 | gcs *gcs 38 | } 39 | 40 | func (s *fakeGCSServer) buildMux() *mux.Router { 41 | jsonHandlers := []struct { 42 | path string 43 | methods []string 44 | handler jsonHandler 45 | }{ 46 | { 47 | path: "/b", 48 | methods: []string{http.MethodPost}, 49 | handler: s.createBucket, 50 | }, 51 | { 52 | path: "/b/{bucketName}/o", 53 | methods: []string{http.MethodGet}, 54 | handler: s.listObjects, 55 | }, 56 | { 57 | path: "/upload/storage/v1/b/{bucketName}/o", 58 | methods: []string{http.MethodPost}, 59 | handler: s.uploadObject, 60 | }, 61 | { 62 | path: "/b/{bucketName}/o/{objectName:.+}", 63 | methods: []string{http.MethodPatch}, 64 | handler: s.patchObject, 65 | }, 66 | { 67 | path: "/b/{bucketName}/o/{objectName:.+}", 68 | methods: []string{http.MethodDelete}, 69 | handler: s.deleteObject, 70 | }, 71 | } 72 | 73 | m := mux.NewRouter() 74 | for _, h := range jsonHandlers { 75 | m.Path(h.path).Methods(h.methods...).HandlerFunc(jsonToHTTPHandler(h.handler)) 76 | } 77 | 78 | // Raw handlers. 79 | m.Path("/b/{bucketName}/o/{objectName:.+}").Methods( 80 | http.MethodGet, http.MethodHead).HandlerFunc(s.getObject) 81 | m.Path("/{bucketName}/{objectName:.+}").Methods( 82 | http.MethodGet, http.MethodHead).HandlerFunc(rawToHTTPHandler(s.downloadObject)) 83 | 84 | return m 85 | } 86 | 87 | func (s *fakeGCSServer) createBucket(req *http.Request) (jsonResponse, error) { 88 | data, err := fromCreateBucketRequest(req) 89 | if err != nil { 90 | return jsonResponse{}, err 91 | } 92 | resp, err := s.gcs.NewBucket(data.Name) 93 | return jsonResponse{Data: resp}, err 94 | } 95 | 96 | func (s *fakeGCSServer) listObjects(req *http.Request) (jsonResponse, error) { 97 | data, err := fromListObjectsRequest(req) 98 | if err != nil { 99 | return jsonResponse{}, err 100 | } 101 | resp, err := s.gcs.ListObjects(data) 102 | return jsonResponse{Data: resp}, err 103 | } 104 | 105 | func (s *fakeGCSServer) uploadObject(req *http.Request) (jsonResponse, error) { 106 | data, err := fromUploadObjectRequest(req) 107 | if err != nil { 108 | return jsonResponse{}, err 109 | } 110 | resp, err := s.gcs.UploadObject(data) 111 | return jsonResponse{Data: resp}, err 112 | } 113 | 114 | func (s *fakeGCSServer) patchObject(req *http.Request) (jsonResponse, error) { 115 | data, err := fromPatchObjectRequest(req) 116 | if err != nil { 117 | return jsonResponse{}, err 118 | } 119 | resp, err := s.gcs.PatchObject(data) 120 | return jsonResponse{Data: resp}, err 121 | } 122 | 123 | func (s *fakeGCSServer) deleteObject(req *http.Request) (jsonResponse, error) { 124 | data, err := fromDeleteObjectRequest(req) 125 | if err != nil { 126 | return jsonResponse{}, err 127 | } 128 | if err := s.gcs.DeleteObject(data); err != nil { 129 | return jsonResponse{}, err 130 | } 131 | return jsonResponse{Status: http.StatusNoContent}, nil 132 | } 133 | 134 | func (s *fakeGCSServer) getObject(w http.ResponseWriter, req *http.Request) { 135 | if alt := req.URL.Query().Get("alt"); alt == "media" || req.Method == http.MethodHead { 136 | rawToHTTPHandler(s.downloadObject)(w, req) 137 | return 138 | } 139 | handler := jsonToHTTPHandler(func(r *http.Request) (jsonResponse, error) { 140 | data, err := fromGetObjectRequest(req) 141 | if err != nil { 142 | return jsonResponse{}, err 143 | } 144 | res, err := s.gcs.DownloadObject(data) 145 | return jsonResponse{Data: res}, err 146 | }) 147 | handler(w, req) 148 | } 149 | 150 | func (s *fakeGCSServer) downloadObject(req *http.Request) (rawResponse, error) { 151 | data, err := fromDownloadObjectRequest(req) 152 | if err != nil { 153 | return rawResponse{}, err 154 | } 155 | res, err := s.gcs.DownloadObject(data) 156 | if err != nil { 157 | return rawResponse{}, err 158 | } 159 | 160 | body := res.Contents 161 | if req.Method == http.MethodHead { 162 | // Do not send contents for HEAD requests. 163 | body = nil 164 | } 165 | header := http.Header{} 166 | header.Set("Accept-Ranges", "bytes") 167 | header.Set("Content-Length", strconv.Itoa(int(res.Size))) 168 | header.Set("X-Goog-Generation", strconv.FormatInt(res.Generation, 10)) 169 | header.Set("X-Goog-Metageneration", strconv.FormatInt(res.Metageneration, 10)) 170 | 171 | return rawResponse{Body: body, Header: header}, nil 172 | } 173 | 174 | type errStatus struct { 175 | error 176 | Code int 177 | } 178 | 179 | func withStatus(err error, status int) error { 180 | return errStatus{err, status} 181 | } 182 | 183 | func statusFromError(err error) int { 184 | if err == nil { 185 | return http.StatusOK 186 | } 187 | var es errStatus 188 | if errors.As(err, &es) { 189 | return es.Code 190 | } 191 | return http.StatusInternalServerError 192 | } 193 | 194 | type jsonResponse struct { 195 | Header http.Header 196 | Data interface{} 197 | Status int 198 | } 199 | 200 | type jsonHandler = func(r *http.Request) (jsonResponse, error) 201 | 202 | func jsonToHTTPHandler(h jsonHandler) http.HandlerFunc { 203 | return func(w http.ResponseWriter, r *http.Request) { 204 | resp, err := h(r) 205 | w.Header().Set("Content-Type", "application/json") 206 | for name, values := range resp.Header { 207 | for _, value := range values { 208 | w.Header().Add(name, value) 209 | } 210 | } 211 | 212 | var ( 213 | status int 214 | data interface{} 215 | ) 216 | 217 | if err != nil { 218 | status = statusFromError(err) 219 | data = newErrorResponse(status, err.Error(), nil) 220 | } else { 221 | status = resp.Status 222 | if status == 0 { 223 | status = http.StatusOK 224 | } 225 | data = resp.Data 226 | } 227 | 228 | w.WriteHeader(status) 229 | _ = json.NewEncoder(w).Encode(data) 230 | } 231 | } 232 | 233 | type rawResponse struct { 234 | Header http.Header 235 | Body []byte 236 | } 237 | 238 | type rawResponseHandler = func(r *http.Request) (rawResponse, error) 239 | 240 | func rawToHTTPHandler(h rawResponseHandler) http.HandlerFunc { 241 | return func(w http.ResponseWriter, r *http.Request) { 242 | resp, err := h(r) 243 | w.Header().Set("Content-Type", "application/octet-stream") 244 | for name, values := range resp.Header { 245 | for _, value := range values { 246 | w.Header().Add(name, value) 247 | } 248 | } 249 | 250 | status := statusFromError(err) 251 | 252 | var data []byte 253 | if status >= 400 { 254 | data = []byte(err.Error()) 255 | } else { 256 | data = resp.Body 257 | } 258 | 259 | w.WriteHeader(status) 260 | if len(data) > 0 { 261 | _, _ = w.Write(data) 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /internal/testkit/fake_service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "io" 21 | "net/http" 22 | "strings" 23 | "testing" 24 | 25 | "cloud.google.com/go/storage" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | "google.golang.org/api/googleapi" 29 | ) 30 | 31 | const testBucketName = "glassdb-test" 32 | 33 | var debugBackend = flag.Bool("debug-backend", false, "debug backend requests") 34 | 35 | func testClient(t *testing.T) *storage.Client { 36 | ctx := context.Background() 37 | client := NewGCSClient(ctx, t, *debugBackend) 38 | if err := client.Bucket(testBucketName).Create(ctx, "test-prj", nil); err != nil { 39 | t.Fatalf("creating test-bucket: %v", err) 40 | } 41 | return client 42 | } 43 | 44 | func TestReadWrite(t *testing.T) { 45 | ctx := context.Background() 46 | client := testClient(t) 47 | bucket := client.Bucket(testBucketName) 48 | obj := bucket.Object("test-obj") 49 | 50 | var ( 51 | gen int64 52 | metagen int64 53 | ) 54 | meta := map[string]string{"k1": "v1"} 55 | 56 | // Write plus metadata. 57 | { 58 | w := obj.NewWriter(ctx) 59 | w.Metadata = meta 60 | w.ContentType = "application/octet-stream" 61 | _, err := w.Write([]byte("value")) 62 | assert.NoError(t, err) 63 | err = w.Close() 64 | assert.NoError(t, err) 65 | assert.Equal(t, meta, w.Attrs().Metadata) 66 | gen = w.Attrs().Generation 67 | metagen = w.Attrs().Metageneration 68 | } 69 | 70 | // Read contents. 71 | { 72 | r, err := obj.NewReader(ctx) 73 | assert.NoError(t, err) 74 | buf, err := io.ReadAll(r) 75 | assert.NoError(t, err) 76 | assert.Equal(t, "value", string(buf)) 77 | assert.NoError(t, r.Close()) 78 | 79 | // Check metadata in reader. 80 | assert.Equal(t, gen, r.Attrs.Generation) 81 | 82 | // Get metadata directly. 83 | attrs, err := obj.Attrs(ctx) 84 | assert.NoError(t, err) 85 | assert.Equal(t, meta, attrs.Metadata) 86 | } 87 | 88 | // Change metadata only. 89 | { 90 | attrs, err := obj.Update(ctx, storage.ObjectAttrsToUpdate{ 91 | Metadata: map[string]string{"k2": "v2"}, 92 | }) 93 | assert.NoError(t, err) 94 | meta = map[string]string{"k1": "v1", "k2": "v2"} 95 | assert.Equal(t, meta, attrs.Metadata) 96 | assert.Equal(t, attrs.Generation, gen) 97 | assert.NotEqual(t, metagen, attrs.Metageneration) 98 | 99 | // Get metadata. 100 | attrs, err = obj.Attrs(ctx) 101 | assert.NoError(t, err) 102 | assert.Equal(t, gen, attrs.Generation) 103 | assert.NotEqual(t, metagen, attrs.Metageneration) 104 | 105 | // Update the new values. 106 | gen = attrs.Generation 107 | metagen = attrs.Metageneration 108 | } 109 | 110 | // Conditions on write. 111 | { 112 | w := obj.If(storage.Conditions{ 113 | GenerationMatch: gen, 114 | MetagenerationMatch: 300, 115 | }).NewWriter(ctx) 116 | w.Metadata = meta 117 | w.ContentType = "application/octet-stream" 118 | _, err := w.Write([]byte("value-2")) 119 | assert.NoError(t, err) 120 | err = w.Close() 121 | var aerr *googleapi.Error 122 | assert.ErrorAs(t, err, &aerr) 123 | assert.Equal(t, http.StatusPreconditionFailed, aerr.Code) 124 | } 125 | 126 | // Conditions on write OK. 127 | { 128 | w := obj.If(storage.Conditions{ 129 | GenerationMatch: gen, 130 | MetagenerationMatch: metagen, 131 | }).NewWriter(ctx) 132 | w.Metadata = meta 133 | w.ContentType = "application/octet-stream" 134 | _, err := w.Write([]byte("value-2")) 135 | assert.NoError(t, err) 136 | err = w.Close() 137 | assert.NoError(t, err) 138 | assert.Equal(t, meta, w.Attrs().Metadata) 139 | } 140 | } 141 | 142 | func TestList(t *testing.T) { 143 | ctx := context.Background() 144 | client := testClient(t) 145 | bucket := client.Bucket(testBucketName) 146 | 147 | testObjs := []string{ 148 | "root", 149 | "a/1", 150 | "a/2", 151 | "a/3", 152 | "a/40", 153 | "a/b/1", 154 | "a/b/2", 155 | "c/1", 156 | } 157 | // Write the test objects. 158 | for _, name := range testObjs { 159 | w := bucket.Object(name).NewWriter(ctx) 160 | w.Metadata = map[string]string{"k": name} 161 | w.ContentType = "application/octet-stream" 162 | _, err := w.Write([]byte(name)) 163 | require.NoError(t, err) 164 | require.NoError(t, w.Close()) 165 | } 166 | 167 | // List tests. 168 | tests := []struct { 169 | name string 170 | prefix string 171 | delimiter string 172 | startOffset string 173 | endOffset string 174 | expected []string 175 | }{ 176 | { 177 | name: "full", 178 | expected: []string{ 179 | "a/1", 180 | "a/2", 181 | "a/3", 182 | "a/40", 183 | "a/b/1", 184 | "a/b/2", 185 | "c/1", 186 | "root", 187 | }, 188 | }, 189 | { 190 | name: "root objs", 191 | delimiter: "/", 192 | expected: []string{ 193 | "root", 194 | "a/", 195 | "c/", 196 | }, 197 | }, 198 | { 199 | name: "subdir", 200 | prefix: "a/", 201 | delimiter: "/", 202 | expected: []string{ 203 | "a/1", 204 | "a/2", 205 | "a/3", 206 | "a/40", 207 | "a/b/", 208 | }, 209 | }, 210 | { 211 | name: "offsets", 212 | prefix: "a/", 213 | delimiter: "/", 214 | startOffset: "a/2", 215 | endOffset: "a/4", 216 | expected: []string{ 217 | "a/2", 218 | "a/3", 219 | }, 220 | }, 221 | } 222 | for _, tc := range tests { 223 | t.Run(tc.name, func(t *testing.T) { 224 | iter := bucket.Objects(ctx, &storage.Query{ 225 | Delimiter: tc.delimiter, 226 | Prefix: tc.prefix, 227 | StartOffset: tc.startOffset, 228 | EndOffset: tc.endOffset, 229 | }) 230 | var res []string 231 | for attrs, err := iter.Next(); err == nil; attrs, err = iter.Next() { 232 | if attrs.Prefix != "" { 233 | res = append(res, attrs.Prefix) 234 | continue 235 | } 236 | // Check that metadata makes sense. 237 | v, ok := attrs.Metadata["k"] 238 | if !ok || !strings.HasPrefix(v, tc.prefix) { 239 | t.Errorf(`metadata["k"] = %q, expected prefix %q`, v, tc.prefix) 240 | } 241 | assert.Equal(t, testBucketName, attrs.Bucket) 242 | res = append(res, attrs.Name) 243 | } 244 | 245 | assert.Equal(t, tc.expected, res) 246 | }) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /internal/testkit/race.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build race 16 | 17 | package testkit 18 | 19 | // RaceEnabled is true when the package was compiled with the go race detector 20 | // enabled. 21 | // 22 | // Not sure of any better way to do it as of writing this. 23 | const RaceEnabled = true 24 | -------------------------------------------------------------------------------- /internal/testkit/race_no.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !race 16 | 17 | package testkit 18 | 19 | // RaceEnabled is true when the package was compiled with the go race detector 20 | // enabled. 21 | // 22 | // Not sure of any better way to do it as of writing this. 23 | const RaceEnabled = false 24 | -------------------------------------------------------------------------------- /internal/testkit/request.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "mime" 24 | "mime/multipart" 25 | "net/http" 26 | "strconv" 27 | 28 | "github.com/gorilla/mux" 29 | ) 30 | 31 | func fromCreateBucketRequest(req *http.Request) (createBucketRequest, error) { 32 | defer req.Body.Close() 33 | res := createBucketRequest{} 34 | err := decodeJSONRequest(&res, req.Body) 35 | return res, err 36 | } 37 | 38 | type createBucketRequest struct { 39 | Name string `json:"name"` 40 | } 41 | 42 | func fromListObjectsRequest(req *http.Request) (listObjectsRequest, error) { 43 | q := req.URL.Query() 44 | if q.Get("versions") == "true" { 45 | return listObjectsRequest{}, fmt.Errorf("versions=true: %w", errNotImplemented) 46 | } 47 | if q.Get("includeTrailingDelimiter") == "true" { 48 | return listObjectsRequest{}, 49 | fmt.Errorf("includeTrailingDelimiter=true: %w", errNotImplemented) 50 | } 51 | return listObjectsRequest{ 52 | Bucket: mux.Vars(req)["bucketName"], 53 | Prefix: q.Get("prefix"), 54 | Delimiter: q.Get("delimiter"), 55 | StartOffset: q.Get("startOffset"), 56 | EndOffset: q.Get("endOffset"), 57 | }, nil 58 | } 59 | 60 | type listObjectsRequest struct { 61 | Bucket string 62 | Prefix string 63 | Delimiter string 64 | StartOffset string 65 | EndOffset string 66 | } 67 | 68 | func fromUploadObjectRequest(req *http.Request) (uploadObjectRequest, error) { 69 | defer req.Body.Close() 70 | 71 | res := uploadObjectRequest{} 72 | 73 | var err error 74 | res.Conditions, err = parseRequestConditions(req) 75 | if err != nil { 76 | return res, err 77 | } 78 | 79 | _, params, err := mime.ParseMediaType(req.Header.Get("Content-Type")) 80 | if err != nil { 81 | return res, withStatus(fmt.Errorf("invalid Content-Type header: %v", err), 82 | http.StatusBadRequest) 83 | } 84 | boundary := params["boundary"] 85 | if boundary == "" { 86 | return res, withStatus(errors.New("expected 'boundary' header"), http.StatusBadRequest) 87 | } 88 | chunks, err := chunksFromMultipart(req.Body, boundary) 89 | if err != nil { 90 | return res, err 91 | } 92 | if len(chunks) != 2 { 93 | return res, withStatus(fmt.Errorf("expected two chunks, got %d", len(chunks)), 94 | http.StatusBadRequest) 95 | } 96 | res.Data = chunks[1] 97 | err = decodeJSONRequest(&res.Metadata, bytes.NewReader(chunks[0])) 98 | return res, err 99 | } 100 | 101 | type uploadObjectRequest struct { 102 | Conditions objectConditions 103 | Metadata uploadMetadata 104 | Data []byte 105 | } 106 | 107 | type uploadMetadata struct { 108 | Bucket string `json:"bucket"` 109 | Name string `json:"name"` 110 | Metadata map[string]string `json:"metadata"` 111 | Crc32 string `json:"crc32c"` 112 | } 113 | 114 | func parseRequestConditions(req *http.Request) (objectConditions, error) { 115 | if err := req.ParseForm(); err != nil { 116 | return objectConditions{}, 117 | withStatus(fmt.Errorf("invalid form: %v", err), http.StatusBadRequest) 118 | } 119 | 120 | parseInt64 := func(name string) *int64 { 121 | val := req.Form.Get(name) 122 | if val == "" { 123 | return nil 124 | } 125 | i, err := strconv.ParseInt(val, 10, 64) 126 | if err != nil { 127 | return nil 128 | } 129 | return &i 130 | } 131 | 132 | return objectConditions{ 133 | IfGenerationMatch: parseInt64("ifGenerationMatch"), 134 | IfMetagenerationMatch: parseInt64("ifMetagenerationMatch"), 135 | IfGenerationNotMatch: parseInt64("ifGenerationNotMatch"), 136 | IfMetagenerationNotMatch: parseInt64("ifMetagenerationNotMatch"), 137 | }, nil 138 | } 139 | 140 | type objectConditions struct { 141 | IfGenerationMatch *int64 142 | IfMetagenerationMatch *int64 143 | IfGenerationNotMatch *int64 144 | IfMetagenerationNotMatch *int64 145 | } 146 | 147 | func fromDownloadObjectRequest(req *http.Request) (downloadObjectRequest, error) { 148 | cond, err := parseRequestConditions(req) 149 | if err != nil { 150 | return downloadObjectRequest{}, err 151 | } 152 | 153 | vars := mux.Vars(req) 154 | return downloadObjectRequest{ 155 | Bucket: vars["bucketName"], 156 | Name: vars["objectName"], 157 | Conditions: cond, 158 | }, nil 159 | } 160 | 161 | type downloadObjectRequest struct { 162 | Bucket string 163 | Name string 164 | Conditions objectConditions 165 | } 166 | 167 | func fromGetObjectRequest(req *http.Request) (downloadObjectRequest, error) { 168 | return fromDownloadObjectRequest(req) 169 | } 170 | 171 | func fromPatchObjectRequest(req *http.Request) (patchObjectRequest, error) { 172 | defer req.Body.Close() 173 | 174 | cond, err := parseRequestConditions(req) 175 | if err != nil { 176 | return patchObjectRequest{}, err 177 | } 178 | 179 | vars := mux.Vars(req) 180 | res := patchObjectRequest{ 181 | Bucket: vars["bucketName"], 182 | Name: vars["objectName"], 183 | Conditions: cond, 184 | } 185 | err = decodeJSONRequest(&res, req.Body) 186 | 187 | return res, err 188 | } 189 | 190 | type patchObjectRequest struct { 191 | Bucket string `json:"bucket"` 192 | Name string 193 | Metadata map[string]string `json:"metadata"` 194 | Conditions objectConditions 195 | } 196 | 197 | func fromDeleteObjectRequest(req *http.Request) (deleteObjectRequest, error) { 198 | defer req.Body.Close() 199 | 200 | cond, err := parseRequestConditions(req) 201 | if err != nil { 202 | return deleteObjectRequest{}, err 203 | } 204 | 205 | vars := mux.Vars(req) 206 | return deleteObjectRequest{ 207 | Bucket: vars["bucketName"], 208 | Name: vars["objectName"], 209 | Conditions: cond, 210 | }, nil 211 | } 212 | 213 | type deleteObjectRequest struct { 214 | Bucket string 215 | Name string 216 | Conditions objectConditions 217 | } 218 | 219 | func decodeJSONRequest(res interface{}, r io.Reader) error { 220 | decoder := json.NewDecoder(r) 221 | if err := decoder.Decode(&res); err != nil { 222 | return withStatus(err, http.StatusBadRequest) 223 | } 224 | return nil 225 | } 226 | 227 | func chunksFromMultipart(r io.Reader, boundary string) ([][]byte, error) { 228 | // The expected format is as follow: 229 | // --boundary 230 | // Content-Type: 231 | // 232 | // 233 | // <... contents> 234 | // --boundary 235 | // ... 236 | // --boundary-- 237 | var chunks [][]byte 238 | 239 | mpr := multipart.NewReader(r, boundary) 240 | for { 241 | part, err := mpr.NextPart() 242 | if err != nil { 243 | if errors.Is(err, io.EOF) { 244 | break 245 | } 246 | return chunks, err 247 | } 248 | chunk, err := io.ReadAll(part) 249 | if err != nil { 250 | return chunks, err 251 | } 252 | chunks = append(chunks, chunk) 253 | } 254 | 255 | return chunks, nil 256 | } 257 | -------------------------------------------------------------------------------- /internal/testkit/slogger.go: -------------------------------------------------------------------------------- 1 | package testkit 2 | 3 | import ( 4 | "bytes" 5 | "log/slog" 6 | "testing" 7 | ) 8 | 9 | // NewLogger creates a new logger that writes to the testing.TB. 10 | func NewLogger(tb testing.TB, opts *slog.HandlerOptions) *slog.Logger { 11 | w := &testLogWriter{ 12 | t: tb, 13 | buf: make([]byte, 0, 1024), 14 | } 15 | tb.Cleanup(w.Close) 16 | return slog.New(slog.NewJSONHandler(w, opts)) 17 | } 18 | 19 | type testLogWriter struct { 20 | t testing.TB 21 | buf []byte 22 | } 23 | 24 | func (w *testLogWriter) Write(p []byte) (n int, err error) { 25 | i := bytes.IndexByte(p, '\n') 26 | if i == -1 { 27 | w.buf = append(w.buf, p...) 28 | return len(p), nil 29 | } 30 | w.buf = append(w.buf, p[:i+1]...) 31 | w.sync(len(w.buf)) 32 | w.buf = append(w.buf, p[i+1:]...) 33 | return len(p), nil 34 | } 35 | 36 | func (w *testLogWriter) Close() { 37 | w.sync(len(w.buf)) 38 | } 39 | 40 | func (w *testLogWriter) sync(n int) { 41 | if n == 0 { 42 | return 43 | } 44 | w.t.Log(string(w.buf[:n])) 45 | w.buf = w.buf[n:] 46 | } 47 | -------------------------------------------------------------------------------- /internal/testkit/stall_backend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testkit 16 | 17 | import ( 18 | "context" 19 | "sync" 20 | 21 | "github.com/sourcegraph/conc" 22 | 23 | "github.com/mbrt/glassdb/backend" 24 | ) 25 | 26 | func NewStallBackend(inner backend.Backend) *StallBackend { 27 | res := &StallBackend{ 28 | Backend: inner, 29 | } 30 | res.cond = sync.NewCond(&res.m) 31 | return res 32 | } 33 | 34 | // StallBackend is a backend where writes can be paused and resumed. 35 | // 36 | // When a write is stalled, it returns immediately with an error. It will complete 37 | // asynchronously only when released. 38 | type StallBackend struct { 39 | backend.Backend 40 | stall bool 41 | release bool 42 | m sync.Mutex 43 | cond *sync.Cond 44 | wg conc.WaitGroup 45 | } 46 | 47 | func (b *StallBackend) WaitForStalled() { 48 | b.wg.Wait() 49 | } 50 | 51 | func (b *StallBackend) StallWrites() { 52 | b.m.Lock() 53 | b.stall = true 54 | b.release = false 55 | b.m.Unlock() 56 | b.cond.Broadcast() 57 | } 58 | 59 | func (b *StallBackend) StopStalling() { 60 | b.m.Lock() 61 | b.stall = false 62 | b.m.Unlock() 63 | b.cond.Broadcast() 64 | } 65 | 66 | func (b *StallBackend) ReleaseStalled() { 67 | b.m.Lock() 68 | b.release = true 69 | b.m.Unlock() 70 | b.cond.Broadcast() 71 | } 72 | 73 | func (b *StallBackend) SetTagsIf( 74 | ctx context.Context, 75 | path string, 76 | expected backend.Version, 77 | t backend.Tags, 78 | ) (backend.Metadata, error) { 79 | b.m.Lock() 80 | if !b.stall { 81 | b.m.Unlock() 82 | return b.Backend.SetTagsIf(ctx, path, expected, t) 83 | } 84 | 85 | b.wg.Go(func() { 86 | for !b.release { 87 | b.cond.Wait() 88 | } 89 | b.m.Unlock() 90 | _, _ = b.Backend.SetTagsIf(ctx, path, expected, t) 91 | }) 92 | return backend.Metadata{}, context.Canceled 93 | } 94 | 95 | func (b *StallBackend) Write( 96 | ctx context.Context, 97 | path string, 98 | value []byte, 99 | t backend.Tags, 100 | ) (backend.Metadata, error) { 101 | b.m.Lock() 102 | if !b.stall { 103 | b.m.Unlock() 104 | return b.Backend.Write(ctx, path, value, t) 105 | } 106 | 107 | b.wg.Go(func() { 108 | for !b.release { 109 | b.cond.Wait() 110 | } 111 | b.m.Unlock() 112 | _, _ = b.Backend.Write(ctx, path, value, t) 113 | }) 114 | return backend.Metadata{}, context.Canceled 115 | } 116 | 117 | func (b *StallBackend) WriteIf( 118 | ctx context.Context, 119 | path string, 120 | value []byte, 121 | expected backend.Version, 122 | t backend.Tags, 123 | ) (backend.Metadata, error) { 124 | b.m.Lock() 125 | if !b.stall { 126 | b.m.Unlock() 127 | return b.Backend.WriteIf(ctx, path, value, expected, t) 128 | } 129 | 130 | b.wg.Go(func() { 131 | for !b.release { 132 | b.cond.Wait() 133 | } 134 | b.m.Unlock() 135 | _, _ = b.Backend.WriteIf(ctx, path, value, expected, t) 136 | }) 137 | return backend.Metadata{}, context.Canceled 138 | } 139 | 140 | func (b *StallBackend) WriteIfNotExists( 141 | ctx context.Context, 142 | path string, 143 | value []byte, 144 | t backend.Tags, 145 | ) (backend.Metadata, error) { 146 | b.m.Lock() 147 | if !b.stall { 148 | b.m.Unlock() 149 | return b.Backend.WriteIfNotExists(ctx, path, value, t) 150 | } 151 | 152 | b.wg.Go(func() { 153 | for !b.release { 154 | b.cond.Wait() 155 | } 156 | b.m.Unlock() 157 | _, _ = b.Backend.WriteIfNotExists(ctx, path, value, t) 158 | }) 159 | return backend.Metadata{}, context.Canceled 160 | } 161 | 162 | func (b *StallBackend) Delete(ctx context.Context, path string) error { 163 | b.m.Lock() 164 | if !b.stall { 165 | b.m.Unlock() 166 | return b.Backend.Delete(ctx, path) 167 | } 168 | 169 | b.wg.Go(func() { 170 | for !b.release { 171 | b.cond.Wait() 172 | } 173 | b.m.Unlock() 174 | _ = b.Backend.Delete(ctx, path) 175 | }) 176 | return context.Canceled 177 | } 178 | 179 | func (b *StallBackend) DeleteIf( 180 | ctx context.Context, 181 | path string, 182 | expected backend.Version, 183 | ) error { 184 | b.m.Lock() 185 | if !b.stall { 186 | b.m.Unlock() 187 | return b.Backend.DeleteIf(ctx, path, expected) 188 | } 189 | 190 | b.wg.Go(func() { 191 | for !b.release { 192 | b.cond.Wait() 193 | } 194 | b.m.Unlock() 195 | _ = b.Backend.DeleteIf(ctx, path, expected) 196 | }) 197 | return context.Canceled 198 | } 199 | -------------------------------------------------------------------------------- /internal/trace/notrace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !tracing 16 | 17 | package trace 18 | 19 | import "context" 20 | 21 | func NewTask(ctx context.Context, _ string) (context.Context, Task) { 22 | return ctx, Task{} 23 | } 24 | 25 | type Task struct{} 26 | 27 | func (Task) End() {} 28 | 29 | func WithRegion(_ context.Context, _ string, fn func()) { 30 | fn() 31 | } 32 | 33 | func StartRegion(context.Context, string) Region { 34 | return Region{} 35 | } 36 | 37 | type Region struct{} 38 | 39 | func (Region) End() {} 40 | -------------------------------------------------------------------------------- /internal/trace/trace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build tracing 16 | 17 | package trace 18 | 19 | import ( 20 | "context" 21 | "runtime/trace" 22 | ) 23 | 24 | func NewTask(ctx context.Context, n string) (context.Context, *trace.Task) { 25 | return trace.NewTask(ctx, n) 26 | } 27 | 28 | func WithRegion(ctx context.Context, n string, fn func()) { 29 | trace.WithRegion(ctx, n, fn) 30 | } 31 | 32 | func StartRegion(ctx context.Context, n string) *trace.Region { 33 | return trace.StartRegion(ctx, n) 34 | } 35 | -------------------------------------------------------------------------------- /internal/trans/gc.go: -------------------------------------------------------------------------------- 1 | package trans 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "sync" 7 | "time" 8 | 9 | "github.com/jonboulle/clockwork" 10 | "github.com/mbrt/glassdb/internal/concurr" 11 | "github.com/mbrt/glassdb/internal/data" 12 | "github.com/mbrt/glassdb/internal/storage" 13 | ) 14 | 15 | const ( 16 | cleanupInterval = time.Minute 17 | sizeLimit = 1024 18 | ) 19 | 20 | func NewGC( 21 | clock clockwork.Clock, 22 | bg *concurr.Background, 23 | tl storage.TLogger, 24 | log *slog.Logger, 25 | ) *GC { 26 | return &GC{ 27 | clock: clock, 28 | bg: bg, 29 | tl: tl, 30 | log: log, 31 | } 32 | } 33 | 34 | type GC struct { 35 | clock clockwork.Clock 36 | bg *concurr.Background 37 | tl storage.TLogger 38 | log *slog.Logger 39 | items []cleanupItem 40 | m sync.Mutex 41 | } 42 | 43 | func (g *GC) Start(ctx context.Context) { 44 | g.bg.Go(ctx, func(ctx context.Context) { 45 | tick := g.clock.NewTicker(cleanupInterval) 46 | 47 | for { 48 | select { 49 | case <-ctx.Done(): 50 | return 51 | case <-tick.Chan(): 52 | g.cleanupRound(ctx) 53 | } 54 | } 55 | }) 56 | } 57 | 58 | func (g *GC) ScheduleTxCleanup(txid data.TxID) { 59 | t := g.clock.Now().Add(cleanupInterval) 60 | 61 | g.m.Lock() 62 | // Avoid growing indefinitely. 63 | if len(g.items) > sizeLimit { 64 | g.m.Unlock() 65 | g.log.Debug("gc: too many items to cleanup") 66 | return 67 | } 68 | g.items = append(g.items, cleanupItem{ 69 | dueTime: t, 70 | txID: txid, 71 | }) 72 | g.m.Unlock() 73 | } 74 | 75 | func (g *GC) cleanupRound(ctx context.Context) { 76 | now := g.clock.Now() 77 | toCleanup := g.filterDueItems(now) 78 | 79 | for _, item := range toCleanup { 80 | if err := g.tl.Delete(ctx, item.txID); err != nil { 81 | g.log.Warn("failed to delete transaction log", "err", err, "txid", item.txID) 82 | } 83 | } 84 | } 85 | 86 | func (g *GC) filterDueItems(now time.Time) []cleanupItem { 87 | g.m.Lock() 88 | defer g.m.Unlock() 89 | 90 | var toCleanup []cleanupItem 91 | i := 0 92 | for ; i < len(g.items); i++ { 93 | if g.items[i].dueTime.After(now) { 94 | break 95 | } 96 | toCleanup = append(toCleanup, g.items[i]) 97 | } 98 | g.items = g.items[i:] 99 | return toCleanup 100 | } 101 | 102 | type cleanupItem struct { 103 | dueTime time.Time 104 | txID data.TxID 105 | } 106 | -------------------------------------------------------------------------------- /internal/trans/gc_test.go: -------------------------------------------------------------------------------- 1 | package trans 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jonboulle/clockwork" 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/mbrt/glassdb/backend" 13 | "github.com/mbrt/glassdb/backend/memory" 14 | "github.com/mbrt/glassdb/internal/cache" 15 | "github.com/mbrt/glassdb/internal/concurr" 16 | "github.com/mbrt/glassdb/internal/data" 17 | "github.com/mbrt/glassdb/internal/errors" 18 | "github.com/mbrt/glassdb/internal/storage" 19 | "github.com/mbrt/glassdb/internal/testkit" 20 | ) 21 | 22 | const conditionTimeout = time.Second 23 | 24 | func TestGC(t *testing.T) { 25 | gc, tctx := newTestGC(t) 26 | start := tctx.clock.Now() 27 | txid := data.TxID([]byte("tx1")) 28 | 29 | // Create a transaction log. 30 | _, err := gc.tl.Set(tctx.ctx, storage.TxLog{ 31 | ID: txid, 32 | Timestamp: start, 33 | Status: storage.TxCommitStatusOK, 34 | }) 35 | assert.NoError(t, err) 36 | 37 | // Make sure the log is there. 38 | tl, err := gc.tl.Get(tctx.ctx, txid) 39 | assert.NoError(t, err) 40 | assert.Equal(t, txid, tl.ID) 41 | 42 | gc.ScheduleTxCleanup(txid) 43 | 44 | // Wait for the GC to run. 45 | tctx.clock.Sleep(cleanupInterval * 2) 46 | 47 | waitForCondition(t, func() bool { 48 | _, err := gc.tl.Get(tctx.ctx, txid) 49 | return errors.Is(err, backend.ErrNotFound) 50 | }) 51 | } 52 | 53 | type gcTestContext struct { 54 | ctx context.Context 55 | clock clockwork.Clock 56 | } 57 | 58 | func newTestGC(t *testing.T) (*GC, gcTestContext) { 59 | t.Helper() 60 | 61 | // It's important to close the context at the end to stop 62 | // any background tasks. 63 | ctx, cancel := context.WithCancel(context.Background()) 64 | t.Cleanup(cancel) 65 | 66 | clock := testkit.NewAcceleratedClock(clockMultiplier) 67 | log := slog.New(nilHandler{}) 68 | cache := cache.New(1024) 69 | local := storage.NewLocal(cache, clock) 70 | global := storage.NewGlobal(memory.New(), local, clock) 71 | tlogger := storage.NewTLogger(clock, global, local, testCollName) 72 | background := concurr.NewBackground() 73 | gc := NewGC(clock, background, tlogger, log) 74 | gc.Start(ctx) 75 | 76 | return gc, gcTestContext{ 77 | ctx, clock, 78 | } 79 | } 80 | 81 | func waitForCondition(t *testing.T, cond func() bool) { 82 | t.Helper() 83 | for start := time.Now(); time.Since(start) < conditionTimeout; { 84 | if cond() { 85 | return 86 | } 87 | time.Sleep(50 * time.Millisecond) 88 | } 89 | t.Errorf("condition not met within 1s") 90 | } 91 | -------------------------------------------------------------------------------- /internal/trans/reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package trans 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "github.com/mbrt/glassdb/backend" 22 | "github.com/mbrt/glassdb/internal/storage" 23 | ) 24 | 25 | func NewReader(l storage.Local, g storage.Global, m *Monitor) Reader { 26 | return Reader{ 27 | local: l, 28 | global: g, 29 | tmon: m, 30 | } 31 | } 32 | 33 | type Reader struct { 34 | local storage.Local 35 | global storage.Global 36 | tmon *Monitor 37 | } 38 | 39 | func (r Reader) Read( 40 | ctx context.Context, 41 | key string, 42 | maxStale time.Duration, 43 | ) (ReadValue, error) { 44 | lr, ok := r.local.Read(key, maxStale) 45 | if ok && !lr.Outdated { 46 | if lr.Deleted { 47 | return ReadValue{}, backend.ErrNotFound 48 | } 49 | lres := ReadValue{ 50 | Value: lr.Value, 51 | Version: lr.Version, 52 | } 53 | return r.handleLockCreate(ctx, key, lres) 54 | } 55 | // Otherwise global read. 56 | gr, err := r.global.Read(ctx, key) 57 | if err != nil { 58 | return ReadValue{}, err 59 | } 60 | gres := ReadValue{ 61 | Value: gr.Value, 62 | Version: storage.Version{ 63 | B: backend.Version{Contents: gr.Version}, 64 | }, 65 | } 66 | return r.handleLockCreate(ctx, key, gres) 67 | } 68 | 69 | func (r Reader) GetMetadata( 70 | ctx context.Context, 71 | key string, 72 | maxStale time.Duration, 73 | ) (backend.Metadata, error) { 74 | lm, ok := r.local.GetMeta(key, maxStale) 75 | if ok && !lm.Outdated { 76 | return lm.M, nil 77 | } 78 | return r.global.GetMetadata(ctx, key) 79 | } 80 | 81 | func (r Reader) handleLockCreate(ctx context.Context, key string, rv ReadValue) (ReadValue, error) { 82 | if len(rv.Value) > 0 { 83 | // We are safe to return this value (it wasn't locked in create). 84 | return rv, nil 85 | } 86 | // Otherwise we might have encountered a key locked in create. 87 | // In which case, we may have read a value that wasn't committed yet. 88 | // Let's check for that with an extra metadata read. 89 | meta, err := r.global.GetMetadata(ctx, key) 90 | if err != nil { 91 | return ReadValue{}, err 92 | } 93 | info, err := storage.TagsLockInfo(meta.Tags) 94 | if err != nil { 95 | return ReadValue{}, err 96 | } 97 | if info.Type != storage.LockTypeCreate { 98 | // No problem. This was really a committed empty value. 99 | return rv, nil 100 | } 101 | // Locked in create. Is the new value available or not? 102 | if len(info.LockedBy) != 1 { 103 | // Something wrong with this lock. Return not found. 104 | return ReadValue{}, backend.ErrNotFound 105 | } 106 | lockerID := info.LockedBy[0] 107 | 108 | // We can check whether this is committed or not. 109 | cv, err := r.tmon.CommittedValue(ctx, key, lockerID) 110 | if err != nil || cv.Status != storage.TxCommitStatusOK || cv.Value.NotWritten { 111 | return ReadValue{}, backend.ErrNotFound 112 | } 113 | // Committed. Let's save ourselves some time and return this. 114 | // Also cache the value for later. 115 | version := storage.Version{Writer: lockerID} 116 | if cv.Value.Deleted { 117 | r.local.MarkDeleted(key, version) 118 | return ReadValue{}, backend.ErrNotFound 119 | } 120 | 121 | r.local.Write(key, cv.Value.Value, version) 122 | 123 | return ReadValue{ 124 | Value: cv.Value.Value, 125 | Version: version, 126 | }, nil 127 | } 128 | 129 | type ReadValue struct { 130 | Value []byte 131 | Version storage.Version 132 | } 133 | -------------------------------------------------------------------------------- /iter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package glassdb 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/mbrt/glassdb/backend" 22 | "github.com/mbrt/glassdb/internal/data/paths" 23 | ) 24 | 25 | type KeysIter struct { 26 | inner backend.ListIter 27 | prefix string 28 | err error 29 | } 30 | 31 | func (it *KeysIter) Next() (key []byte, ok bool) { 32 | backendPath, ok := it.inner.Next() 33 | if !ok { 34 | return nil, false 35 | } 36 | 37 | if it.prefix == "" { 38 | r, err := paths.Parse(backendPath) 39 | if err != nil { 40 | it.err = fmt.Errorf("parsing path %q: %w", backendPath, err) 41 | return nil, false 42 | } 43 | if r.Type != paths.KeyType { 44 | it.err = fmt.Errorf("got path type %q, expected %q", r.Type, paths.KeyType) 45 | return nil, false 46 | } 47 | it.prefix = r.Prefix + "/" 48 | } 49 | trimmed := strings.TrimPrefix(backendPath, it.prefix) 50 | k, err := paths.ToKey(trimmed) 51 | if err != nil { 52 | it.err = err 53 | return nil, false 54 | } 55 | return k, true 56 | } 57 | 58 | func (it *KeysIter) Err() error { 59 | if it.err != nil { 60 | return it.err 61 | } 62 | return it.inner.Err() 63 | } 64 | 65 | type CollectionsIter struct { 66 | inner backend.ListIter 67 | prefix string 68 | err error 69 | } 70 | 71 | func (it *CollectionsIter) Next() (name []byte, ok bool) { 72 | backendPath, ok := it.inner.Next() 73 | if !ok { 74 | return nil, false 75 | } 76 | // Iterating over directories causes a trailing slash we need to remove. 77 | backendPath = strings.TrimSuffix(backendPath, "/") 78 | 79 | if it.prefix == "" { 80 | r, err := paths.Parse(backendPath) 81 | if err != nil { 82 | it.err = fmt.Errorf("parsing path %q: %w", backendPath, err) 83 | return nil, false 84 | } 85 | if r.Type != paths.CollectionType { 86 | it.err = fmt.Errorf("got path type %q, expected %q", r.Type, paths.CollectionType) 87 | return nil, false 88 | } 89 | it.prefix = r.Prefix + "/" 90 | } 91 | trimmed := strings.TrimPrefix(backendPath, it.prefix) 92 | c, err := paths.ToCollection(trimmed) 93 | if err != nil { 94 | it.err = err 95 | return nil, false 96 | } 97 | return c, true 98 | } 99 | 100 | func (it *CollectionsIter) Err() error { 101 | if it.err != nil { 102 | return it.err 103 | } 104 | return it.inner.Err() 105 | } 106 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package glassdb 16 | 17 | import ( 18 | "context" 19 | "sync/atomic" 20 | "time" 21 | 22 | "github.com/mbrt/glassdb/backend" 23 | ) 24 | 25 | type Stats struct { 26 | // Transactions statistics. 27 | TxN int // number of completed transactions. 28 | TxTime time.Duration // time spent within transactions. 29 | TxReads int // number of reads. 30 | TxWrites int // number of writes. 31 | TxRetries int // number of retried transactions. 32 | 33 | // Backend statistics. 34 | MetaReads int // number of read metadata. 35 | MetaWrites int // number of write metadata. 36 | ObjReads int // number of read objects. 37 | ObjWrites int // number of written objects. 38 | ObjLists int // number of list calls. 39 | } 40 | 41 | // Sub calculates and returns the difference between two sets of transaction 42 | // stats. This is useful when obtaining stats at two different points in time 43 | // and you need the performance counters occurred within that time span. 44 | func (s Stats) Sub(other Stats) Stats { 45 | return Stats{ 46 | TxN: s.TxN - other.TxN, 47 | TxTime: s.TxTime - other.TxTime, 48 | TxReads: s.TxReads - other.TxReads, 49 | TxWrites: s.TxWrites - other.TxWrites, 50 | TxRetries: s.TxRetries - other.TxRetries, 51 | 52 | MetaReads: s.MetaReads - other.MetaReads, 53 | MetaWrites: s.MetaWrites - other.MetaWrites, 54 | ObjReads: s.ObjReads - other.ObjReads, 55 | ObjWrites: s.ObjWrites - other.ObjWrites, 56 | ObjLists: s.ObjLists - other.ObjLists, 57 | } 58 | } 59 | 60 | func (s *Stats) add(other *Stats) { 61 | s.TxN += other.TxN 62 | s.TxTime += other.TxTime 63 | s.TxReads += other.TxReads 64 | s.TxWrites += other.TxWrites 65 | s.TxRetries += other.TxRetries 66 | 67 | s.MetaReads += other.MetaReads 68 | s.MetaWrites += other.MetaWrites 69 | s.ObjReads += other.ObjReads 70 | s.ObjWrites += other.ObjWrites 71 | s.ObjLists += other.ObjLists 72 | } 73 | 74 | type statsBackend struct { 75 | inner backend.Backend 76 | 77 | metaReads int32 78 | metaWrites int32 79 | objReads int32 80 | objWrites int32 81 | objLists int32 82 | } 83 | 84 | // StatsAndReset returns backend stats and resets them. 85 | func (b *statsBackend) StatsAndReset() Stats { 86 | return Stats{ 87 | MetaReads: int(atomic.SwapInt32(&b.metaReads, 0)), 88 | MetaWrites: int(atomic.SwapInt32(&b.metaWrites, 0)), 89 | ObjReads: int(atomic.SwapInt32(&b.objReads, 0)), 90 | ObjWrites: int(atomic.SwapInt32(&b.objWrites, 0)), 91 | ObjLists: int(atomic.SwapInt32(&b.objLists, 0)), 92 | } 93 | } 94 | 95 | func (b *statsBackend) ReadIfModified( 96 | ctx context.Context, path string, version int64, 97 | ) (backend.ReadReply, error) { 98 | atomic.AddInt32(&b.objReads, 1) 99 | return b.inner.ReadIfModified(ctx, path, version) 100 | } 101 | 102 | func (b *statsBackend) Read( 103 | ctx context.Context, path string, 104 | ) (backend.ReadReply, error) { 105 | atomic.AddInt32(&b.objReads, 1) 106 | return b.inner.Read(ctx, path) 107 | } 108 | 109 | func (b *statsBackend) GetMetadata( 110 | ctx context.Context, path string, 111 | ) (backend.Metadata, error) { 112 | atomic.AddInt32(&b.metaReads, 1) 113 | return b.inner.GetMetadata(ctx, path) 114 | } 115 | 116 | func (b *statsBackend) SetTagsIf( 117 | ctx context.Context, 118 | path string, 119 | expected backend.Version, 120 | t backend.Tags, 121 | ) (backend.Metadata, error) { 122 | atomic.AddInt32(&b.metaWrites, 1) 123 | return b.inner.SetTagsIf(ctx, path, expected, t) 124 | } 125 | 126 | func (b *statsBackend) Write( 127 | ctx context.Context, path string, value []byte, t backend.Tags, 128 | ) (backend.Metadata, error) { 129 | atomic.AddInt32(&b.objWrites, 1) 130 | return b.inner.Write(ctx, path, value, t) 131 | } 132 | 133 | func (b *statsBackend) WriteIf( 134 | ctx context.Context, 135 | path string, 136 | value []byte, 137 | expected backend.Version, 138 | t backend.Tags, 139 | ) (backend.Metadata, error) { 140 | atomic.AddInt32(&b.objWrites, 1) 141 | return b.inner.WriteIf(ctx, path, value, expected, t) 142 | } 143 | 144 | func (b *statsBackend) WriteIfNotExists( 145 | ctx context.Context, path string, value []byte, t backend.Tags, 146 | ) (backend.Metadata, error) { 147 | atomic.AddInt32(&b.objWrites, 1) 148 | return b.inner.WriteIfNotExists(ctx, path, value, t) 149 | } 150 | 151 | func (b *statsBackend) Delete(ctx context.Context, path string) error { 152 | atomic.AddInt32(&b.objWrites, 1) 153 | return b.inner.Delete(ctx, path) 154 | } 155 | 156 | func (b *statsBackend) DeleteIf( 157 | ctx context.Context, path string, expected backend.Version, 158 | ) error { 159 | atomic.AddInt32(&b.objWrites, 1) 160 | return b.inner.DeleteIf(ctx, path, expected) 161 | } 162 | 163 | func (b *statsBackend) List(ctx context.Context, dirPath string) (backend.ListIter, error) { 164 | atomic.AddInt32(&b.objLists, 1) 165 | return b.inner.List(ctx, dirPath) 166 | } 167 | 168 | // Ensure backendStatser implements Backend. 169 | var _ backend.Backend = (*statsBackend)(nil) 170 | -------------------------------------------------------------------------------- /tx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The glassdb Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package glassdb 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | 22 | "github.com/mbrt/glassdb/backend" 23 | "github.com/mbrt/glassdb/internal/data/paths" 24 | "github.com/mbrt/glassdb/internal/storage" 25 | "github.com/mbrt/glassdb/internal/trans" 26 | "github.com/sourcegraph/conc" 27 | ) 28 | 29 | var ErrAborted = errors.New("aborted transaction") 30 | 31 | func newTx( 32 | ctx context.Context, 33 | g storage.Global, 34 | l storage.Local, 35 | tm *trans.Monitor, 36 | ) *Tx { 37 | return &Tx{ 38 | ctx: ctx, 39 | global: g, 40 | local: l, 41 | reader: trans.NewReader(l, g, tm), 42 | staged: make(map[string]tvalue), 43 | reads: make(map[string]readInfo), 44 | } 45 | } 46 | 47 | type Tx struct { 48 | ctx context.Context 49 | global storage.Global 50 | local storage.Local 51 | reader trans.Reader 52 | staged map[string]tvalue 53 | reads map[string]readInfo 54 | aborted bool 55 | retried bool 56 | } 57 | 58 | func (t *Tx) Read(c Collection, key []byte) ([]byte, error) { 59 | p := paths.FromKey(c.prefix, key) 60 | // Return the last read, if present. 61 | tval, ok := t.staged[p] 62 | if ok { 63 | return tval.val, nil 64 | } 65 | if info, ok := t.reads[p]; ok && !info.found { 66 | // Be consistent with values not found the first time. 67 | // Do not try to fetch again, to avoid phantom reads. 68 | return nil, backend.ErrNotFound 69 | } 70 | 71 | // First time we read this in the transaction. 72 | rv, err := t.reader.Read(t.ctx, p, storage.MaxStaleness) 73 | if errors.Is(err, backend.ErrNotFound) { 74 | // Cache the fact that the value wasn't present. 75 | // Avoids phantom reads. 76 | t.reads[p] = readInfo{found: false} 77 | return nil, err 78 | } 79 | if err != nil { 80 | return nil, fmt.Errorf("reading from storage: %w", err) 81 | } 82 | // Cache locally and mark the read. 83 | t.staged[p] = tvalue{val: rv.Value} 84 | t.reads[p] = readInfo{ 85 | version: trans.ReadVersion{ 86 | Version: rv.Version.B.Contents, 87 | LastWriter: rv.Version.Writer, 88 | }, 89 | found: true, 90 | } 91 | return rv.Value, nil 92 | } 93 | 94 | func (t *Tx) ReadMulti(ks []FQKey) []ReadResult { 95 | if len(ks) == 0 { 96 | return nil 97 | } 98 | 99 | res := make([]ReadResult, len(ks)) 100 | infos := make([]readInfoEx, len(ks)) 101 | wg := conc.WaitGroup{} 102 | 103 | for i, key := range ks { 104 | i := i // https://golang.org/doc/faq#closures_and_goroutines 105 | 106 | p := paths.FromKey(key.Collection.prefix, key.Key) 107 | infos[i].path = p 108 | 109 | // Return the last read, if present. 110 | tval, ok := t.staged[p] 111 | if ok { 112 | res[i] = ReadResult{Value: tval.val} 113 | infos[i].cached = true 114 | continue 115 | } 116 | if info, ok := t.reads[p]; ok && !info.found { 117 | // Be consistent with values not found the first time. 118 | // Do not try to fetch again, to avoid phantom reads. 119 | res[i] = ReadResult{Err: backend.ErrNotFound} 120 | infos[i].cached = true 121 | continue 122 | } 123 | 124 | // Fetch in parallel. 125 | wg.Go(func() { 126 | rv, err := t.reader.Read(t.ctx, p, storage.MaxStaleness) 127 | if err != nil && !errors.Is(err, backend.ErrNotFound) { 128 | err = fmt.Errorf("reading from storage: %w", err) 129 | } 130 | res[i] = ReadResult{Value: rv.Value, Err: err} 131 | if err != nil { 132 | return 133 | } 134 | infos[i].version = trans.ReadVersion{ 135 | Version: rv.Version.B.Contents, 136 | LastWriter: rv.Version.Writer, 137 | } 138 | infos[i].found = true 139 | }) 140 | } 141 | 142 | wg.Wait() 143 | 144 | // Update the local staged with what was just fetched. 145 | // We need to do it serially to avoid race conditions or locking. 146 | for i, info := range infos { 147 | if info.cached { 148 | continue 149 | } 150 | if !info.found { 151 | t.reads[info.path] = readInfo{found: false} 152 | continue 153 | } 154 | t.staged[info.path] = tvalue{val: res[i].Value} 155 | t.reads[info.path] = readInfo{ 156 | version: info.version, 157 | found: true, 158 | } 159 | } 160 | 161 | return res 162 | } 163 | 164 | func (t *Tx) Write(c Collection, key, value []byte) error { 165 | if key == nil { 166 | return errors.New("invalid nil key") 167 | } 168 | p := paths.FromKey(c.prefix, key) 169 | t.staged[p] = tvalue{val: value, modified: true} 170 | return nil 171 | } 172 | 173 | func (t *Tx) Delete(c Collection, key []byte) error { 174 | if key == nil { 175 | return errors.New("invalid nil key") 176 | } 177 | p := paths.FromKey(c.prefix, key) 178 | t.staged[p] = tvalue{deleted: true} 179 | return nil 180 | } 181 | 182 | func (t *Tx) Abort() error { 183 | t.aborted = true 184 | return ErrAborted 185 | } 186 | 187 | func (t *Tx) reset() { 188 | t.staged = make(map[string]tvalue) 189 | t.reads = make(map[string]readInfo) 190 | t.retried = true 191 | } 192 | 193 | func (t *Tx) collectAccesses() trans.Data { 194 | var reads []trans.ReadAccess 195 | var writes []trans.WriteAccess 196 | 197 | // Collect writes. 198 | for k, v := range t.staged { 199 | if !v.modified && !v.deleted { 200 | continue 201 | } 202 | writes = append(writes, trans.WriteAccess{ 203 | Path: k, 204 | Val: v.val, 205 | Delete: v.deleted, 206 | }) 207 | } 208 | // Collect reads. 209 | for k, v := range t.reads { 210 | reads = append(reads, trans.ReadAccess{ 211 | Path: k, 212 | Version: v.version, 213 | Found: v.found, 214 | }) 215 | } 216 | 217 | return trans.Data{ 218 | Reads: reads, 219 | Writes: writes, 220 | } 221 | } 222 | 223 | // FQKey is a fully qualified key: collection + key name. 224 | type FQKey struct { 225 | Collection Collection 226 | Key []byte 227 | } 228 | 229 | type ReadResult struct { 230 | Value []byte 231 | Err error 232 | } 233 | 234 | type tvalue struct { 235 | val []byte 236 | modified bool 237 | deleted bool 238 | } 239 | 240 | type readInfo struct { 241 | version trans.ReadVersion 242 | found bool 243 | } 244 | 245 | type readInfoEx struct { 246 | path string 247 | version trans.ReadVersion 248 | found bool 249 | cached bool 250 | } 251 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package glassdb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "path" 8 | 9 | "github.com/mbrt/glassdb/backend" 10 | ) 11 | 12 | const ( 13 | dbVersion = "v0" 14 | dbMetaPath = "glassdb" 15 | 16 | dbVersionTag = "version" 17 | ) 18 | 19 | func checkOrCreateDBMeta(ctx context.Context, b backend.Backend, name string) error { 20 | err := checkDBVersion(ctx, b, name) 21 | if err == nil { 22 | return nil 23 | } 24 | if !errors.Is(err, backend.ErrNotFound) { 25 | return err 26 | } 27 | err = setDBMetadata(ctx, b, name, dbVersion) 28 | if err == nil { 29 | return nil 30 | } 31 | if !errors.Is(err, backend.ErrPrecondition) { 32 | return fmt.Errorf("creating db metadata: %w", err) 33 | } 34 | // We raced against another instance creating the metadata. 35 | // Check it again. 36 | return checkDBVersion(ctx, b, name) 37 | } 38 | 39 | func checkDBVersion(ctx context.Context, b backend.Backend, name string) error { 40 | p := path.Join(name, dbMetaPath) 41 | meta, err := b.GetMetadata(ctx, p) 42 | if err != nil { 43 | return err 44 | } 45 | if v := meta.Tags[dbVersionTag]; v != dbVersion { 46 | return fmt.Errorf("got db version %q, expected %q", v, dbVersion) 47 | } 48 | return nil 49 | } 50 | 51 | func setDBMetadata(ctx context.Context, b backend.Backend, name, version string) error { 52 | p := path.Join(name, dbMetaPath) 53 | _, err := b.WriteIfNotExists(ctx, p, nil, backend.Tags{ 54 | dbVersionTag: version, 55 | }) 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package glassdb 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/mbrt/glassdb/backend/memory" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCreateDBMeta(t *testing.T) { 12 | ctx := context.Background() 13 | b := memory.New() 14 | err := setDBMetadata(ctx, b, "test", dbVersion) 15 | assert.NoError(t, err) 16 | 17 | err = checkDBVersion(ctx, b, "test") 18 | assert.NoError(t, err) 19 | } 20 | 21 | func TestCheckWrongDBMeta(t *testing.T) { 22 | ctx := context.Background() 23 | b := memory.New() 24 | err := setDBMetadata(ctx, b, "test", "v300") 25 | assert.NoError(t, err) 26 | 27 | err = checkDBVersion(ctx, b, "test") 28 | assert.ErrorContains(t, err, "db version") 29 | } 30 | --------------------------------------------------------------------------------