2 Commits

Author SHA1 Message Date
eleksir
ac243bf4fd Replace ioutil.ReadFile with os.ReadFile, fix chat set [[:space:]] in regexp 2023-06-14 01:08:24 +03:00
eleksir
747ff1454e Upgrade versions of vendored libs 2023-06-14 01:06:17 +03:00
124 changed files with 5136 additions and 1258 deletions

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
@@ -131,7 +130,7 @@ func ReadConfig() MyConfig {
continue continue
} }
buf, err := ioutil.ReadFile(location) buf, err := os.ReadFile(location)
// Не удалось прочитать, попробуем следующего кандидата // Не удалось прочитать, попробуем следующего кандидата
if err != nil { if err != nil {

View File

@@ -388,32 +388,32 @@ func msgParser(ctx context.Context, msg string) {
j.Message = fortune() j.Message = fortune()
} else if (cmd == "karma") || (cmd == "карма") { } else if (cmd == "karma") || (cmd == "карма") {
j.Message = getKarma(j.ChatID, "") j.Message = getKarma(j.ChatID, "")
} else if match, _ := regexp.MatchString("^(rum|ром)[::space::]?([::space::].+)*$", cmd); match { } else if match, _ := regexp.MatchString("^(rum|ром)[[:space:]]?([[:space:]].+)*$", cmd); match {
format := "/me притаскивает на подносе стопку рома для %s, края стопки искрятся кристаллами соли." format := "/me притаскивает на подносе стопку рома для %s, края стопки искрятся кристаллами соли."
j.Message = fmt.Sprintf(format, j.Misc.Username) j.Message = fmt.Sprintf(format, j.Misc.Username)
} else if match, _ := regexp.MatchString("^(vodka|водка)[::space::]?([::space::].+)*$", cmd); match { } else if match, _ := regexp.MatchString("^(vodka|водка)[[:space:]]?([[:space:]].+)*$", cmd); match {
format := "/me подаёт шот водки с небольшим маринованным огурчиком на блюдце для %s. Из огурчика торчит " format := "/me подаёт шот водки с небольшим маринованным огурчиком на блюдце для %s. Из огурчика торчит "
format += "небольшая вилочка." format += "небольшая вилочка."
j.Message = fmt.Sprintf(format, j.Misc.Username) j.Message = fmt.Sprintf(format, j.Misc.Username)
} else if match, _ := regexp.MatchString("^(beer|пиво)[::space::]?([::space::].+)*$", cmd); match { } else if match, _ := regexp.MatchString("^(beer|пиво)[[:space:]]?([[:space:]].+)*$", cmd); match {
format := "/me бахает об стол перед %s кружкой холодного пива, часть пенной шапки сползает по запотевшей " format := "/me бахает об стол перед %s кружкой холодного пива, часть пенной шапки сползает по запотевшей "
format += "стенке кружки." format += "стенке кружки."
j.Message = fmt.Sprintf(format, j.Misc.Username) j.Message = fmt.Sprintf(format, j.Misc.Username)
} else if match, _ := regexp.MatchString("^(tequila|текила)[::space::]?([::space::].+)*$", cmd); match { } else if match, _ := regexp.MatchString("^(tequila|текила)[[:space:]]?([[:space:]].+)*$", cmd); match {
format := "/me ставит рядом с %s шот текилы, аккуратно на ребро стопки насаживает дольку лайма и ставит " format := "/me ставит рядом с %s шот текилы, аккуратно на ребро стопки насаживает дольку лайма и ставит "
format += "кофейное блюдце с горочкой соли." format += "кофейное блюдце с горочкой соли."
j.Message = fmt.Sprintf(format, j.Misc.Username) j.Message = fmt.Sprintf(format, j.Misc.Username)
} else if match, _ := regexp.MatchString("^(whisky|виски)[::space::]?([::space::].+)*$", cmd); match { } else if match, _ := regexp.MatchString("^(whisky|виски)[[:space:]]?([[:space:]].+)*$", cmd); match {
format := "/me демонстративно достаёт из морозилки пару кубических камушков, бросает их в толстодонный " format := "/me демонстративно достаёт из морозилки пару кубических камушков, бросает их в толстодонный "
format += "стакан и аккуратно наливает Jack Daniels. Запускает стакан вдоль барной стойки, он " format += "стакан и аккуратно наливает Jack Daniels. Запускает стакан вдоль барной стойки, он "
format += "останавливается около %s." format += "останавливается около %s."
j.Message = fmt.Sprintf(format, j.Misc.Username) j.Message = fmt.Sprintf(format, j.Misc.Username)
} else if match, _ := regexp.MatchString("^(absinth|абсент)[::space::]?([::space::].+)*$", cmd); match { } else if match, _ := regexp.MatchString("^(absinth|абсент)[[:space:]]?([[:space:]].+)*$", cmd); match {
format := "/me наливает абсент в стопку. Смочив кубик сахара в абсенте кладёт его на дырявую ложечку и " format := "/me наливает абсент в стопку. Смочив кубик сахара в абсенте кладёт его на дырявую ложечку и "
format += "поджигает. Как только пламя потухнет, %s размешивает оплавившийся кубик в абсенте и подносит " format += "поджигает. Как только пламя потухнет, %s размешивает оплавившийся кубик в абсенте и подносит "
format += "стопку %s." format += "стопку %s."
j.Message = fmt.Sprintf(format, j.Misc.BotNick, j.Misc.Username) j.Message = fmt.Sprintf(format, j.Misc.BotNick, j.Misc.Username)
} else if match, _ := regexp.MatchString("^fuck[::space::]*$", cmd); match { } else if match, _ := regexp.MatchString("^fuck[[:space:]]*$", cmd); match {
j.Message = "Не ругайся." j.Message = "Не ругайся."
} else { } else {
cmdLen := len(cmd) cmdLen := len(cmd)

20
go.mod
View File

@@ -3,11 +3,11 @@ module main
go 1.19 go 1.19
require ( require (
github.com/cockroachdb/errors v1.9.1 github.com/cockroachdb/errors v1.10.0
github.com/cockroachdb/pebble v0.0.0-20230517202617-f6eaf9a696e6 github.com/cockroachdb/pebble v0.0.0-20230613192303-a8a7ebf58839
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/hjson/hjson-go v3.3.0+incompatible github.com/hjson/hjson-go v3.3.0+incompatible
github.com/sirupsen/logrus v1.9.2 github.com/sirupsen/logrus v1.9.3
) )
require ( require (
@@ -15,24 +15,24 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.4 // indirect github.com/cockroachdb/redact v1.1.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/getsentry/sentry-go v0.21.0 // indirect github.com/getsentry/sentry-go v0.21.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/compress v1.16.6 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.15.1 // indirect github.com/prometheus/client_golang v1.15.1 // indirect
github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.43.0 // indirect github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/sys v0.8.0 // indirect golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/text v0.10.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
) )

312
go.sum
View File

@@ -1,388 +1,120 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= github.com/cockroachdb/errors v1.10.0 h1:lfxS8zZz1+OjtV4MtNWgboi/W5tyLEB6VQZBXN+0VUU=
github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= github.com/cockroachdb/errors v1.10.0/go.mod h1:lknhIsEVQ9Ss/qKDBQS/UqFSvPQjOwNq2qyKAxtHRqE=
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v0.0.0-20230517202617-f6eaf9a696e6 h1:qmPygPODBjUWt7dT3kR118Yn7y0Ol6ULetf78jj/fNQ= github.com/cockroachdb/pebble v0.0.0-20230613192303-a8a7ebf58839 h1:avSJHOeU0yPVq2rSBuBLlBC7Q6hU1J3IEPAt9fK7bvY=
github.com/cockroachdb/pebble v0.0.0-20230517202617-f6eaf9a696e6/go.mod h1:TkdVsGYRqtULUppt2RbC+YaKtTHnHoWa2apfFrSKABw= github.com/cockroachdb/pebble v0.0.0-20230613192303-a8a7ebf58839/go.mod h1:TkdVsGYRqtULUppt2RbC+YaKtTHnHoWa2apfFrSKABw=
github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.4 h1:Y0XrVI2FAyofNyGveodTN//qdpPtFKcKTeCBsK3AHAQ= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/redact v1.1.4/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c=
github.com/getsentry/sentry-go v0.21.0 h1:c9l5F1nPF30JIppulk4veau90PK6Smu3abgVtVQWon4= github.com/getsentry/sentry-go v0.21.0 h1:c9l5F1nPF30JIppulk4veau90PK6Smu3abgVtVQWon4=
github.com/getsentry/sentry-go v0.21.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/getsentry/sentry-go v0.21.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI= github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strings" "strings"
@@ -50,7 +49,7 @@ func main() {
// "Заглатывать" файл целиком - плохая практика, но мы предополагаем, что файлы будут небольшие. // "Заглатывать" файл целиком - плохая практика, но мы предополагаем, что файлы будут небольшие.
fortuneFile := fmt.Sprintf("%s/%s", srcDir, file.Name()) fortuneFile := fmt.Sprintf("%s/%s", srcDir, file.Name())
bytesRead, err := ioutil.ReadFile(fortuneFile) bytesRead, err := os.ReadFile(fortuneFile)
if err != nil { if err != nil {
log.Errorf("Unable to read %s: %s\n", file.Name(), err) log.Errorf("Unable to read %s: %s\n", file.Name(), err)

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -80,7 +79,7 @@ func main() {
} }
file := config.DataDir + "/friday_src/friday.txt" file := config.DataDir + "/friday_src/friday.txt"
bytesRead, err := ioutil.ReadFile(file) bytesRead, err := os.ReadFile(file)
if err != nil { if err != nil {
log.Errorf("Unable to read %s: %s\n", file, err) log.Errorf("Unable to read %s: %s\n", file, err)

View File

@@ -16,6 +16,8 @@ all:
@echo " make stressmeta" @echo " make stressmeta"
@echo " make crossversion-meta" @echo " make crossversion-meta"
@echo " make mod-update" @echo " make mod-update"
@echo " make generate"
@echo " make generate-test-data"
@echo " make clean" @echo " make clean"
override testflags := override testflags :=
@@ -69,6 +71,16 @@ stress-crossversion:
generate: generate:
${GO} generate ${PKG} ${GO} generate ${PKG}
generate:
# Note that the output of generate-test-data is not deterministic. This should
# only be run manually as needed.
.PHONY: generate-test-data
generate-test-data:
${GO} run -tags make_incorrect_manifests ./tool/make_incorrect_manifests.go
${GO} run -tags make_test_find_db ./tool/make_test_find_db.go
${GO} run -tags make_test_sstables ./tool/make_test_sstables.go
mod-update: mod-update:
${GO} get -u ${GO} get -u
${GO} mod tidy ${GO} mod tidy

View File

@@ -265,6 +265,13 @@ type Batch struct {
// then it will only contain key kinds of IngestSST. // then it will only contain key kinds of IngestSST.
ingestedSSTBatch bool ingestedSSTBatch bool
// minimumFormatMajorVersion indicates the format major version required in
// order to commit this batch. If an operation requires a particular format
// major version, it ratchets the batch's minimumFormatMajorVersion. When
// the batch is committed, this is validated against the database's current
// format major version.
minimumFormatMajorVersion FormatMajorVersion
// Synchronous Apply uses the commit WaitGroup for both publishing the // Synchronous Apply uses the commit WaitGroup for both publishing the
// seqnum and waiting for the WAL fsync (if needed). Asynchronous // seqnum and waiting for the WAL fsync (if needed). Asynchronous
// ApplyNoSyncWait, which implies WriteOptions.Sync is true, uses the commit // ApplyNoSyncWait, which implies WriteOptions.Sync is true, uses the commit
@@ -413,6 +420,7 @@ func (b *Batch) refreshMemTableSize() {
b.countRangeDels = 0 b.countRangeDels = 0
b.countRangeKeys = 0 b.countRangeKeys = 0
b.minimumFormatMajorVersion = 0
for r := b.Reader(); ; { for r := b.Reader(); ; {
kind, key, value, ok := r.Next() kind, key, value, ok := r.Next()
if !ok { if !ok {
@@ -423,12 +431,22 @@ func (b *Batch) refreshMemTableSize() {
b.countRangeDels++ b.countRangeDels++
case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete:
b.countRangeKeys++ b.countRangeKeys++
case InternalKeyKindDeleteSized:
if b.minimumFormatMajorVersion < ExperimentalFormatDeleteSizedAndObsolete {
b.minimumFormatMajorVersion = ExperimentalFormatDeleteSizedAndObsolete
}
case InternalKeyKindIngestSST: case InternalKeyKindIngestSST:
if b.minimumFormatMajorVersion < FormatFlushableIngest {
b.minimumFormatMajorVersion = FormatFlushableIngest
}
// This key kind doesn't contribute to the memtable size. // This key kind doesn't contribute to the memtable size.
continue continue
} }
b.memTableSize += memTableEntrySize(len(key), len(value)) b.memTableSize += memTableEntrySize(len(key), len(value))
} }
if b.countRangeKeys > 0 && b.minimumFormatMajorVersion < FormatRangeKeys {
b.minimumFormatMajorVersion = FormatRangeKeys
}
} }
// Apply the operations contained in the batch to the receiver batch. // Apply the operations contained in the batch to the receiver batch.
@@ -598,6 +616,41 @@ func (b *Batch) prepareDeferredKeyRecord(keyLen int, kind InternalKeyKind) {
b.data = b.data[:pos+keyLen] b.data = b.data[:pos+keyLen]
} }
// AddInternalKey allows the caller to add an internal key of point key kinds to
// a batch. Passing in an internal key of kind RangeKey* or RangeDelete will
// result in a panic. Note that the seqnum in the internal key is effectively
// ignored, even though the Kind is preserved. This is because the batch format
// does not allow for a per-key seqnum to be specified, only a batch-wide one.
//
// Note that non-indexed keys (IngestKeyKind{LogData,IngestSST}) are not
// supported with this method as they require specialized logic.
func (b *Batch) AddInternalKey(key *base.InternalKey, value []byte, _ *WriteOptions) error {
keyLen := len(key.UserKey)
hasValue := false
switch key.Kind() {
case InternalKeyKindRangeDelete, InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete:
panic("unexpected range delete or range key kind in AddInternalKey")
case InternalKeyKindSingleDelete, InternalKeyKindDelete:
b.prepareDeferredKeyRecord(len(key.UserKey), key.Kind())
default:
b.prepareDeferredKeyValueRecord(keyLen, len(value), key.Kind())
hasValue = true
}
b.deferredOp.index = b.index
copy(b.deferredOp.Key, key.UserKey)
if hasValue {
copy(b.deferredOp.Value, value)
}
// TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining
// in go1.13 will remove the need for this.
if b.index != nil {
if err := b.index.Add(b.deferredOp.offset); err != nil {
return err
}
}
return nil
}
// Set adds an action to the batch that sets the key to map to the value. // Set adds an action to the batch that sets the key to map to the value.
// //
// It is safe to modify the contents of the arguments after Set returns. // It is safe to modify the contents of the arguments after Set returns.
@@ -680,6 +733,72 @@ func (b *Batch) DeleteDeferred(keyLen int) *DeferredBatchOp {
return &b.deferredOp return &b.deferredOp
} }
// DeleteSized behaves identically to Delete, but takes an additional
// argument indicating the size of the value being deleted. DeleteSized
// should be preferred when the caller has the expectation that there exists
// a single internal KV pair for the key (eg, the key has not been
// overwritten recently), and the caller knows the size of its value.
//
// DeleteSized will record the value size within the tombstone and use it to
// inform compaction-picking heuristics which strive to reduce space
// amplification in the LSM. This "calling your shot" mechanic allows the
// storage engine to more accurately estimate and reduce space amplification.
//
// It is safe to modify the contents of the arguments after DeleteSized
// returns.
func (b *Batch) DeleteSized(key []byte, deletedValueSize uint32, _ *WriteOptions) error {
deferredOp := b.DeleteSizedDeferred(len(key), deletedValueSize)
copy(b.deferredOp.Key, key)
// TODO(peter): Manually inline DeferredBatchOp.Finish(). Check if in a
// later Go release this is unnecessary.
if b.index != nil {
if err := b.index.Add(deferredOp.offset); err != nil {
return err
}
}
return nil
}
// DeleteSizedDeferred is similar to DeleteSized in that it adds a sized delete
// operation to the batch, except it only takes in key length instead of a
// complete key slice, letting the caller encode into the DeferredBatchOp.Key
// slice and then call Finish() on the returned object.
func (b *Batch) DeleteSizedDeferred(keyLen int, deletedValueSize uint32) *DeferredBatchOp {
if b.minimumFormatMajorVersion < ExperimentalFormatDeleteSizedAndObsolete {
b.minimumFormatMajorVersion = ExperimentalFormatDeleteSizedAndObsolete
}
// Encode the sum of the key length and the value in the value.
v := uint64(deletedValueSize) + uint64(keyLen)
// Encode `v` as a varint.
var buf [binary.MaxVarintLen64]byte
n := 0
{
x := v
for x >= 0x80 {
buf[n] = byte(x) | 0x80
x >>= 7
n++
}
buf[n] = byte(x)
n++
}
// NB: In batch entries and sstable entries, values are stored as
// varstrings. Here, the value is itself a simple varint. This results in an
// unnecessary double layer of encoding:
// varint(n) varint(deletedValueSize)
// The first varint will always be 1-byte, since a varint-encoded uint64
// will never exceed 128 bytes. This unnecessary extra byte and wrapping is
// preserved to avoid special casing across the database, and in particular
// in sstable block decoding which is performance sensitive.
b.prepareDeferredKeyValueRecord(keyLen, n, InternalKeyKindDeleteSized)
b.deferredOp.index = b.index
copy(b.deferredOp.Value, buf[:n])
return &b.deferredOp
}
// SingleDelete adds an action to the batch that single deletes the entry for key. // SingleDelete adds an action to the batch that single deletes the entry for key.
// See Writer.SingleDelete for more details on the semantics of SingleDelete. // See Writer.SingleDelete for more details on the semantics of SingleDelete.
// //
@@ -782,6 +901,9 @@ func (b *Batch) rangeKeySetDeferred(startLen, internalValueLen int) *DeferredBat
func (b *Batch) incrementRangeKeysCount() { func (b *Batch) incrementRangeKeysCount() {
b.countRangeKeys++ b.countRangeKeys++
if b.minimumFormatMajorVersion < FormatRangeKeys {
b.minimumFormatMajorVersion = FormatRangeKeys
}
if b.index != nil { if b.index != nil {
b.rangeKeys = nil b.rangeKeys = nil
b.rangeKeysSeqNum = 0 b.rangeKeysSeqNum = 0
@@ -895,6 +1017,7 @@ func (b *Batch) ingestSST(fileNum base.FileNum) {
// is not reset because for the InternalKeyKindIngestSST the count is the // is not reset because for the InternalKeyKindIngestSST the count is the
// number of sstable paths which have been added to the batch. // number of sstable paths which have been added to the batch.
b.memTableSize = origMemTableSize b.memTableSize = origMemTableSize
b.minimumFormatMajorVersion = FormatFlushableIngest
} }
// Empty returns true if the batch is empty, and false otherwise. // Empty returns true if the batch is empty, and false otherwise.
@@ -1199,6 +1322,7 @@ func (b *Batch) Reset() {
b.commitStats = BatchCommitStats{} b.commitStats = BatchCommitStats{}
b.commitErr = nil b.commitErr = nil
b.applied.Store(false) b.applied.Store(false)
b.minimumFormatMajorVersion = 0
if b.data != nil { if b.data != nil {
if cap(b.data) > batchMaxRetainedSize { if cap(b.data) > batchMaxRetainedSize {
// If the capacity of the buffer is larger than our maximum // If the capacity of the buffer is larger than our maximum
@@ -1365,7 +1489,8 @@ func (r *BatchReader) Next() (kind InternalKeyKind, ukey []byte, value []byte, o
} }
switch kind { switch kind {
case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete,
InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete,
InternalKeyKindDeleteSized:
*r, value, ok = batchDecodeStr(*r) *r, value, ok = batchDecodeStr(*r)
if !ok { if !ok {
return 0, nil, nil, false return 0, nil, nil, false
@@ -1500,7 +1625,8 @@ func (i *batchIter) value() []byte {
switch InternalKeyKind(data[offset]) { switch InternalKeyKind(data[offset]) {
case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete,
InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete,
InternalKeyKindDeleteSized:
_, value, ok := batchDecodeStr(data[keyEnd:]) _, value, ok := batchDecodeStr(data[keyEnd:])
if !ok { if !ok {
return nil return nil
@@ -1944,7 +2070,8 @@ func (i *flushableBatchIter) value() base.LazyValue {
var ok bool var ok bool
switch kind { switch kind {
case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete,
InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete,
InternalKeyKindDeleteSized:
keyEnd := i.offsets[i.index].keyEnd keyEnd := i.offsets[i.index].keyEnd
_, value, ok = batchDecodeStr(i.data[keyEnd:]) _, value, ok = batchDecodeStr(i.data[keyEnd:])
if !ok { if !ok {

View File

@@ -1333,10 +1333,10 @@ func (c *compaction) newInputIter(
// internal iterator interface). The resulting merged rangedel iterator is // internal iterator interface). The resulting merged rangedel iterator is
// then included with the point levels in a single mergingIter. // then included with the point levels in a single mergingIter.
newRangeDelIter := func( newRangeDelIter := func(
f manifest.LevelFile, _ *IterOptions, bytesIterated *uint64, f manifest.LevelFile, _ *IterOptions, l manifest.Level, bytesIterated *uint64,
) (keyspan.FragmentIterator, error) { ) (keyspan.FragmentIterator, error) {
iter, rangeDelIter, err := newIters(context.Background(), f.FileMetadata, iter, rangeDelIter, err := newIters(context.Background(), f.FileMetadata,
nil /* iter options */, internalIterOpts{bytesIterated: &c.bytesIterated}) &IterOptions{level: l}, internalIterOpts{bytesIterated: &c.bytesIterated})
if err == nil { if err == nil {
// TODO(peter): It is mildly wasteful to open the point iterator only to // TODO(peter): It is mildly wasteful to open the point iterator only to
// immediately close it. One way to solve this would be to add new // immediately close it. One way to solve this would be to add new
@@ -1415,6 +1415,10 @@ func (c *compaction) newInputIter(
iterOpts := IterOptions{logger: c.logger} iterOpts := IterOptions{logger: c.logger}
// TODO(bananabrick): Get rid of the extra manifest.Level parameter and fold it into // TODO(bananabrick): Get rid of the extra manifest.Level parameter and fold it into
// compactionLevel. // compactionLevel.
//
// TODO(bilal): when we start using strict obsolete sstables for L5 and L6
// in disaggregated storage, and rely on the obsolete bit, we will also need
// to configure the levelIter at these levels to hide the obsolete points.
addItersForLevel := func(level *compactionLevel, l manifest.Level) error { addItersForLevel := func(level *compactionLevel, l manifest.Level) error {
iters = append(iters, newLevelIter(iterOpts, c.cmp, nil /* split */, newIters, iters = append(iters, newLevelIter(iterOpts, c.cmp, nil /* split */, newIters,
level.files.Iter(), l, &c.bytesIterated)) level.files.Iter(), l, &c.bytesIterated))
@@ -1455,8 +1459,10 @@ func (c *compaction) newInputIter(
// mergingIter. // mergingIter.
iter := level.files.Iter() iter := level.files.Iter()
for f := iter.First(); f != nil; f = iter.Next() { for f := iter.First(); f != nil; f = iter.Next() {
rangeDelIter, err := newRangeDelIter(iter.Take(), nil, &c.bytesIterated) rangeDelIter, err := newRangeDelIter(iter.Take(), nil, l, &c.bytesIterated)
if err != nil { if err != nil {
// The error will already be annotated with the BackingFileNum, so
// we annotate it with the FileNum.
return errors.Wrapf(err, "pebble: could not open table %s", errors.Safe(f.FileNum)) return errors.Wrapf(err, "pebble: could not open table %s", errors.Safe(f.FileNum))
} }
if rangeDelIter != emptyKeyspanIter { if rangeDelIter != emptyKeyspanIter {
@@ -1474,7 +1480,7 @@ func (c *compaction) newInputIter(
} }
if hasRangeKeys { if hasRangeKeys {
li := &keyspan.LevelIter{} li := &keyspan.LevelIter{}
newRangeKeyIterWrapper := func(file *manifest.FileMetadata, iterOptions *keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) { newRangeKeyIterWrapper := func(file *manifest.FileMetadata, iterOptions keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) {
iter, err := newRangeKeyIter(file, iterOptions) iter, err := newRangeKeyIter(file, iterOptions)
if iter != nil { if iter != nil {
// Ensure that the range key iter is not closed until the compaction is // Ensure that the range key iter is not closed until the compaction is
@@ -1495,7 +1501,7 @@ func (c *compaction) newInputIter(
} }
return iter, err return iter, err
} }
li.Init(keyspan.SpanIterOptions{}, c.cmp, newRangeKeyIterWrapper, level.files.Iter(), l, manifest.KeyTypeRange) li.Init(keyspan.SpanIterOptions{Level: l}, c.cmp, newRangeKeyIterWrapper, level.files.Iter(), l, manifest.KeyTypeRange)
rangeKeyIters = append(rangeKeyIters, li) rangeKeyIters = append(rangeKeyIters, li)
} }
return nil return nil
@@ -2066,12 +2072,12 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) {
bytesFlushed = c.bytesIterated bytesFlushed = c.bytesIterated
d.mu.snapshots.cumulativePinnedCount += stats.cumulativePinnedKeys d.mu.snapshots.cumulativePinnedCount += stats.cumulativePinnedKeys
d.mu.snapshots.cumulativePinnedSize += stats.cumulativePinnedSize d.mu.snapshots.cumulativePinnedSize += stats.cumulativePinnedSize
d.mu.versions.metrics.Keys.MissizedTombstonesCount += stats.countMissizedDels
d.maybeUpdateDeleteCompactionHints(c) d.maybeUpdateDeleteCompactionHints(c)
d.clearCompactingState(c, err != nil) d.clearCompactingState(c, err != nil)
delete(d.mu.compact.inProgress, c) delete(d.mu.compact.inProgress, c)
d.mu.versions.incrementCompactions(c.kind, c.extraLevels) d.mu.versions.incrementCompactions(c.kind, c.extraLevels)
d.mu.versions.incrementCompactionBytes(-c.bytesWritten)
var flushed flushableList var flushed flushableList
if err == nil { if err == nil {
@@ -2559,9 +2565,20 @@ func (d *DB) compact1(c *compaction, errChannel chan error) (err error) {
info.Duration = d.timeNow().Sub(startTime) info.Duration = d.timeNow().Sub(startTime)
if err == nil { if err == nil {
d.mu.versions.logLock() d.mu.versions.logLock()
// Confirm if any of this compaction's inputs were deleted while this
// compaction was ongoing.
for i := range c.inputs {
c.inputs[i].files.Each(func(m *manifest.FileMetadata) {
if m.Deleted {
err = firstError(err, errors.New("pebble: file deleted by a concurrent operation, will retry compaction"))
}
})
}
if err == nil {
err = d.mu.versions.logAndApply(jobID, ve, c.metrics, false /* forceRotation */, func() []compactionInfo { err = d.mu.versions.logAndApply(jobID, ve, c.metrics, false /* forceRotation */, func() []compactionInfo {
return d.getInProgressCompactionInfoLocked(c) return d.getInProgressCompactionInfoLocked(c)
}) })
}
if err != nil { if err != nil {
// TODO(peter): untested. // TODO(peter): untested.
for _, f := range pendingOutputs { for _, f := range pendingOutputs {
@@ -2617,6 +2634,7 @@ func (d *DB) compact1(c *compaction, errChannel chan error) (err error) {
type compactStats struct { type compactStats struct {
cumulativePinnedKeys uint64 cumulativePinnedKeys uint64
cumulativePinnedSize uint64 cumulativePinnedSize uint64
countMissizedDels uint64
} }
// runCompactions runs a compaction that produces new on-disk tables from // runCompactions runs a compaction that produces new on-disk tables from
@@ -2850,11 +2868,13 @@ func (d *DB) runCompaction(
Path: d.objProvider.Path(objMeta), Path: d.objProvider.Path(objMeta),
FileNum: fileNum, FileNum: fileNum,
}) })
if c.kind != compactionKindFlush {
writable = &compactionWritable{ writable = &compactionWritable{
Writable: writable, Writable: writable,
versions: d.mu.versions, versions: d.mu.versions,
written: &c.bytesWritten, written: &c.bytesWritten,
} }
}
createdFiles = append(createdFiles, fileNum.DiskFileNum()) createdFiles = append(createdFiles, fileNum.DiskFileNum())
cacheOpts := private.SSTableCacheOpts(d.cacheID, fileNum.DiskFileNum()).(sstable.WriterOption) cacheOpts := private.SSTableCacheOpts(d.cacheID, fileNum.DiskFileNum()).(sstable.WriterOption)
@@ -3232,7 +3252,10 @@ func (d *DB) runCompaction(
return nil, pendingOutputs, stats, err return nil, pendingOutputs, stats, err
} }
} }
if err := tw.Add(*key, val); err != nil { // iter.snapshotPinned is broader than whether the point was covered by
// a RANGEDEL, but it is harmless to pass true when the callee will also
// independently discover that the point is obsolete.
if err := tw.AddWithForceObsolete(*key, val, iter.snapshotPinned); err != nil {
return nil, pendingOutputs, stats, err return nil, pendingOutputs, stats, err
} }
if iter.snapshotPinned { if iter.snapshotPinned {
@@ -3285,6 +3308,11 @@ func (d *DB) runCompaction(
} }
} }
// The compaction iterator keeps track of a count of the number of DELSIZED
// keys that encoded an incorrect size. Propagate it up as a part of
// compactStats.
stats.countMissizedDels = iter.stats.countMissizedDels
if err := d.objProvider.Sync(); err != nil { if err := d.objProvider.Sync(); err != nil {
return nil, pendingOutputs, stats, err return nil, pendingOutputs, stats, err
} }
@@ -3613,7 +3641,7 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) {
if len(filesToDelete) > 0 { if len(filesToDelete) > 0 {
d.deleters.Add(1) d.deleters.Add(1)
// Delete asynchronously if that could get held up in the pacer. // Delete asynchronously if that could get held up in the pacer.
if d.opts.Experimental.MinDeletionRate > 0 { if d.opts.TargetByteDeletionRate > 0 {
go d.paceAndDeleteObsoleteFiles(jobID, filesToDelete) go d.paceAndDeleteObsoleteFiles(jobID, filesToDelete)
} else { } else {
d.paceAndDeleteObsoleteFiles(jobID, filesToDelete) d.paceAndDeleteObsoleteFiles(jobID, filesToDelete)
@@ -3626,14 +3654,20 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) {
func (d *DB) paceAndDeleteObsoleteFiles(jobID int, files []obsoleteFile) { func (d *DB) paceAndDeleteObsoleteFiles(jobID int, files []obsoleteFile) {
defer d.deleters.Done() defer d.deleters.Done()
pacer := (pacer)(nilPacer) pacer := (pacer)(nilPacer)
if d.opts.Experimental.MinDeletionRate > 0 { if d.opts.TargetByteDeletionRate > 0 {
pacer = newDeletionPacer(d.deletionLimiter, d.getDeletionPacerInfo) pacer = newDeletionPacer(d.deletionLimiter, d.getDeletionPacerInfo)
} }
for _, of := range files { for _, of := range files {
path := base.MakeFilepath(d.opts.FS, of.dir, of.fileType, of.fileNum) path := base.MakeFilepath(d.opts.FS, of.dir, of.fileType, of.fileNum)
if of.fileType == fileTypeTable { if of.fileType == fileTypeTable {
// Don't throttle deletion of shared objects.
meta, err := d.objProvider.Lookup(of.fileType, of.fileNum)
// If we get an error here, deleteObsoleteObject won't actually delete
// anything, so we don't need to throttle.
if err == nil && !meta.IsShared() {
_ = pacer.maybeThrottle(of.fileSize) _ = pacer.maybeThrottle(of.fileSize)
}
d.mu.Lock() d.mu.Lock()
d.mu.versions.metrics.Table.ObsoleteCount-- d.mu.versions.metrics.Table.ObsoleteCount--
d.mu.versions.metrics.Table.ObsoleteSize -= of.fileSize d.mu.versions.metrics.Table.ObsoleteSize -= of.fileSize

View File

@@ -6,6 +6,7 @@ package pebble
import ( import (
"bytes" "bytes"
"encoding/binary"
"fmt" "fmt"
"io" "io"
"sort" "sort"
@@ -205,6 +206,21 @@ type compactionIter struct {
// compaction iterator was only returned because an open snapshot prevents // compaction iterator was only returned because an open snapshot prevents
// its elision. This field only applies to point keys, and not to range // its elision. This field only applies to point keys, and not to range
// deletions or range keys. // deletions or range keys.
//
// snapshotPinned is also used to set the forceObsolete value in the call to
// Writer.AddWithForceObsolete. Note that in that call, it is sufficient to
// mark all keys obsoleted by RANGEDELs as forceObsolete=true and that the
// implementation of Writer.AddWithForceObsolete will itself discover other
// causes of obsolescence. We mention this since in the presence of MERGE,
// obsolescence due to multiple keys at the same user key is not fully
// represented by snapshotPinned=true:
//
// For MERGE, it is possible that doing the merge is interrupted even when
// the next point key is in the same stripe. This can happen if the loop in
// mergeNext gets interrupted by sameStripeNonSkippable.
// sameStripeNonSkippable occurs due to RANGEDELs that sort before
// SET/MERGE/DEL with the same seqnum, so the RANGEDEL does not necessarily
// delete the subsequent SET/MERGE/DEL keys.
snapshotPinned bool snapshotPinned bool
// The index of the snapshot for the current key within the snapshots slice. // The index of the snapshot for the current key within the snapshots slice.
curSnapshotIdx int curSnapshotIdx int
@@ -240,6 +256,10 @@ type compactionIter struct {
// The on-disk format major version. This informs the types of keys that // The on-disk format major version. This informs the types of keys that
// may be written to disk during a compaction. // may be written to disk during a compaction.
formatVersion FormatMajorVersion formatVersion FormatMajorVersion
stats struct {
// count of DELSIZED keys that were missized.
countMissizedDels uint64
}
} }
func newCompactionIter( func newCompactionIter(
@@ -310,10 +330,10 @@ func (i *compactionIter) Next() (*InternalKey, []byte) {
// respect to `iterKey` and related state: // respect to `iterKey` and related state:
// //
// - `!skip && pos == iterPosNext`: `iterKey` is already at the next key. // - `!skip && pos == iterPosNext`: `iterKey` is already at the next key.
// - `!skip && pos == iterPosCur`: We are at the key that has been returned. // - `!skip && pos == iterPosCurForward`: We are at the key that has been returned.
// To move forward we advance by one key, even if that lands us in the same // To move forward we advance by one key, even if that lands us in the same
// snapshot stripe. // snapshot stripe.
// - `skip && pos == iterPosCur`: We are at the key that has been returned. // - `skip && pos == iterPosCurForward`: We are at the key that has been returned.
// To move forward we skip skippable entries in the stripe. // To move forward we skip skippable entries in the stripe.
if i.pos == iterPosCurForward { if i.pos == iterPosCurForward {
if i.skip { if i.skip {
@@ -382,11 +402,18 @@ func (i *compactionIter) Next() (*InternalKey, []byte) {
} else if cover == keyspan.CoversInvisibly { } else if cover == keyspan.CoversInvisibly {
// i.iterKey would be deleted by a range deletion if there weren't // i.iterKey would be deleted by a range deletion if there weren't
// any open snapshots. Mark it as pinned. // any open snapshots. Mark it as pinned.
//
// NB: there are multiple places in this file where we call
// i.rangeDelFrag.Covers and this is the only one where we are writing
// to i.snapshotPinned. Those other cases occur in mergeNext where the
// caller is deciding whether the value should be merged or not, and the
// key is in the same snapshot stripe. Hence, snapshotPinned is by
// definition false in those cases.
i.snapshotPinned = true i.snapshotPinned = true
} }
switch i.iterKey.Kind() { switch i.iterKey.Kind() {
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
if i.elideTombstone(i.iterKey.UserKey) { if i.elideTombstone(i.iterKey.UserKey) {
if i.curSnapshotIdx == 0 { if i.curSnapshotIdx == 0 {
// If we're at the last snapshot stripe and the tombstone // If we're at the last snapshot stripe and the tombstone
@@ -410,6 +437,12 @@ func (i *compactionIter) Next() (*InternalKey, []byte) {
i.skip = true i.skip = true
return &i.key, i.value return &i.key, i.value
case InternalKeyKindDeleteSized:
// We may skip subsequent keys because of this tombstone. Scan
// ahead to see just how much data this tombstone drops and if
// the tombstone's value should be updated accordingly.
return i.deleteSizedNext()
case InternalKeyKindSingleDelete: case InternalKeyKindSingleDelete:
if i.singleDeleteNext() { if i.singleDeleteNext() {
return &i.key, i.value return &i.key, i.value
@@ -686,11 +719,12 @@ func (i *compactionIter) setNext() {
i.skip = true i.skip = true
return return
case sameStripeSkippable: case sameStripeSkippable:
// We're still in the same stripe. If this is a DEL/SINGLEDEL, we // We're still in the same stripe. If this is a
// stop looking and emit a SETWITHDEL. Subsequent keys are // DEL/SINGLEDEL/DELSIZED, we stop looking and emit a SETWITHDEL.
// eligible for skipping. // Subsequent keys are eligible for skipping.
if i.iterKey.Kind() == InternalKeyKindDelete || if i.iterKey.Kind() == InternalKeyKindDelete ||
i.iterKey.Kind() == InternalKeyKindSingleDelete { i.iterKey.Kind() == InternalKeyKindSingleDelete ||
i.iterKey.Kind() == InternalKeyKindDeleteSized {
i.key.SetKind(InternalKeyKindSetWithDelete) i.key.SetKind(InternalKeyKindSetWithDelete)
i.skip = true i.skip = true
return return
@@ -715,7 +749,7 @@ func (i *compactionIter) mergeNext(valueMerger ValueMerger) stripeChangeType {
} }
key := i.iterKey key := i.iterKey
switch key.Kind() { switch key.Kind() {
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
// We've hit a deletion tombstone. Return everything up to this point and // We've hit a deletion tombstone. Return everything up to this point and
// then skip entries until the next snapshot stripe. We change the kind // then skip entries until the next snapshot stripe. We change the kind
// of the result key to a Set so that it shadows keys in lower // of the result key to a Set so that it shadows keys in lower
@@ -802,9 +836,9 @@ func (i *compactionIter) singleDeleteNext() bool {
key := i.iterKey key := i.iterKey
switch key.Kind() { switch key.Kind() {
case InternalKeyKindDelete, InternalKeyKindMerge, InternalKeyKindSetWithDelete: case InternalKeyKindDelete, InternalKeyKindMerge, InternalKeyKindSetWithDelete, InternalKeyKindDeleteSized:
// We've hit a Delete, Merge or SetWithDelete, transform the // We've hit a Delete, DeleteSized, Merge, SetWithDelete, transform
// SingleDelete into a full Delete. // the SingleDelete into a full Delete.
i.key.SetKind(InternalKeyKindDelete) i.key.SetKind(InternalKeyKindDelete)
i.skip = true i.skip = true
return true return true
@@ -825,6 +859,110 @@ func (i *compactionIter) singleDeleteNext() bool {
} }
} }
// deleteSizedNext processes a DELSIZED point tombstone. Unlike ordinary DELs,
// these tombstones carry a value that's a varint indicating the size of the
// entry (len(key)+len(value)) that the tombstone is expected to delete.
//
// When a deleteSizedNext is encountered, we skip ahead to see which keys, if
// any, are elided as a result of the tombstone.
func (i *compactionIter) deleteSizedNext() (*base.InternalKey, []byte) {
i.saveKey()
i.valid = true
i.skip = true
// The DELSIZED tombstone may have no value at all. This happens when the
// tombstone has already deleted the key that the user originally predicted.
// In this case, we still peek forward in case there's another DELSIZED key
// with a lower sequence number, in which case we'll adopt its value.
if len(i.iterValue) == 0 {
i.value = i.valueBuf[:0]
} else {
i.valueBuf = append(i.valueBuf[:0], i.iterValue...)
i.value = i.valueBuf
}
// Loop through all the keys within this stripe that are skippable,
// totalling the size of their keys and values.
i.pos = iterPosNext
var elidedSize uint64
for i.nextInStripe() == sameStripeSkippable {
elidedSize += uint64(len(i.iterKey.UserKey)) + uint64(len(i.iterValue))
if i.iterKey.Kind() == InternalKeyKindDeleteSized {
// We encountered a DELSIZED that's deleted by the original
// DELSIZED. These tombstones could've been intended to delete two
// distinct values, eg:
//
// a.DELSIZED.9 a.SET.7 a.DELSIZED.5 a.SET.4
//
// If a.DELSIZED.9 has already deleted a.SET.7, its size has already
// been zeroed out. In this case, we want to adopt the value of the
// DELSIZED with the lower sequence number, in case the a.SET.4 key
// has not yet been elided.
i.valueBuf = append(i.valueBuf[:0], i.iterValue...)
i.value = i.valueBuf
// Reset the elided total.
elidedSize = 0
}
}
// Reset skip if we landed outside the original stripe. Otherwise, we landed
// in the same stripe on a non-skippable key. In that case we should preserve
// `i.skip == true` such that later keys in the stripe will continue to be
// skipped.
if i.iterStripeChange == newStripeNewKey || i.iterStripeChange == newStripeSameKey {
i.skip = false
}
if elidedSize == 0 {
// If we didn't find any keys that we could elide due to this tombstone
// (or at least none since the last DELSIZED encountered), we can return
// the key and our existing saved value verbatim.
return &i.key, i.value
}
// Some key(s) were elided as a result of this tombstone. Decode the
// tombstone's value to see if it matches the amount of data actually
// elided.
var v uint64
if len(i.value) > 0 {
var n int
v, n = binary.Uvarint(i.value)
if n != len(i.value) {
i.err = base.CorruptionErrorf("DELSIZED holds invalid value: %x", errors.Safe(i.value))
i.valid = false
return nil, nil
}
}
// If the user's prediction of the value size was correct, then from now on
// it's predicted to delete nothing. Return an empty value.
//
// NB: It's important that we still always return the tombstone, because
// DELSIZED's values are a best effort optimization, not a correctness
// mechanic. There is no guarantee that this key was only set once before
// the DELSIZED was written.
// If the tombstone carried a larger size than what was actually elided,
// it's unclear what to do. The user-provided size was wrong, so it's
// unlikely to be accurate or meaningful. We could:
//
// 1. return the DELSIZED with the original user-provided size unmodified
// 2. return the DELZIZED with a zeroed size to reflect that a key was
// elided, even if it wasn't the anticipated size.
// 3. subtract the elided size from the estimate and re-encode.
// 4. convert the DELSIZED into a value-less DEL, so that ordinary DEL
// heuristics apply.
//
// We opt for (4) under the rationale that we can't rely on the
// user-provided size for accuracy, so ordinary DEL heuristics are safer.
i.value = i.valueBuf[:0]
if elidedSize != v {
i.stats.countMissizedDels++
i.key.SetKind(InternalKeyKindDelete)
}
return &i.key, i.value
}
func (i *compactionIter) saveKey() { func (i *compactionIter) saveKey() {
i.keyBuf = append(i.keyBuf[:0], i.iterKey.UserKey...) i.keyBuf = append(i.keyBuf[:0], i.iterKey.UserKey...)
i.key.UserKey = i.keyBuf i.key.UserKey = i.keyBuf

View File

@@ -674,15 +674,16 @@ type candidateLevelInfo struct {
file manifest.LevelFile file manifest.LevelFile
} }
func fileCompensation(f *fileMetadata) uint64 {
return uint64(f.Stats.PointDeletionsBytesEstimate) + f.Stats.RangeDeletionsBytesEstimate
}
// compensatedSize returns f's file size, inflated according to compaction // compensatedSize returns f's file size, inflated according to compaction
// priorities. // priorities.
func compensatedSize(f *fileMetadata, pointTombstoneWeight float64) uint64 { func compensatedSize(f *fileMetadata) uint64 {
sz := f.Size // Add in the estimate of disk space that may be reclaimed by compacting the
// Add in the estimate of disk space that may be reclaimed by compacting // file's tombstones.
// the file's tombstones. return f.Size + fileCompensation(f)
sz += uint64(float64(f.Stats.PointDeletionsBytesEstimate) * pointTombstoneWeight)
sz += f.Stats.RangeDeletionsBytesEstimate
return sz
} }
// compensatedSizeAnnotator implements manifest.Annotator, annotating B-Tree // compensatedSizeAnnotator implements manifest.Annotator, annotating B-Tree
@@ -691,7 +692,6 @@ func compensatedSize(f *fileMetadata, pointTombstoneWeight float64) uint64 {
// asynchronously, so its values are marked as cacheable only if a file's // asynchronously, so its values are marked as cacheable only if a file's
// stats have been loaded. // stats have been loaded.
type compensatedSizeAnnotator struct { type compensatedSizeAnnotator struct {
pointTombstoneWeight float64
} }
var _ manifest.Annotator = compensatedSizeAnnotator{} var _ manifest.Annotator = compensatedSizeAnnotator{}
@@ -709,7 +709,7 @@ func (a compensatedSizeAnnotator) Accumulate(
f *fileMetadata, dst interface{}, f *fileMetadata, dst interface{},
) (v interface{}, cacheOK bool) { ) (v interface{}, cacheOK bool) {
vptr := dst.(*uint64) vptr := dst.(*uint64)
*vptr = *vptr + compensatedSize(f, a.pointTombstoneWeight) *vptr = *vptr + compensatedSize(f)
return vptr, f.StatsValid() return vptr, f.StatsValid()
} }
@@ -724,10 +724,10 @@ func (a compensatedSizeAnnotator) Merge(src interface{}, dst interface{}) interf
// iterator. Note that this function is linear in the files available to the // iterator. Note that this function is linear in the files available to the
// iterator. Use the compensatedSizeAnnotator if querying the total // iterator. Use the compensatedSizeAnnotator if querying the total
// compensated size of a level. // compensated size of a level.
func totalCompensatedSize(iter manifest.LevelIterator, pointTombstoneWeight float64) uint64 { func totalCompensatedSize(iter manifest.LevelIterator) uint64 {
var sz uint64 var sz uint64
for f := iter.First(); f != nil; f = iter.Next() { for f := iter.First(); f != nil; f = iter.Next() {
sz += compensatedSize(f, pointTombstoneWeight) sz += compensatedSize(f)
} }
return sz return sz
} }
@@ -925,9 +925,7 @@ func (p *compactionPickerByScore) initLevelMaxBytes(inProgressCompactions []comp
} }
} }
func calculateSizeAdjust( func calculateSizeAdjust(inProgressCompactions []compactionInfo) [numLevels]int64 {
inProgressCompactions []compactionInfo, pointTombstoneWeight float64,
) [numLevels]int64 {
// Compute a size adjustment for each level based on the in-progress // Compute a size adjustment for each level based on the in-progress
// compactions. We subtract the compensated size of start level inputs. // compactions. We subtract the compensated size of start level inputs.
// Since compensated file sizes may be compensated because they reclaim // Since compensated file sizes may be compensated because they reclaim
@@ -937,10 +935,16 @@ func calculateSizeAdjust(
var sizeAdjust [numLevels]int64 var sizeAdjust [numLevels]int64
for i := range inProgressCompactions { for i := range inProgressCompactions {
c := &inProgressCompactions[i] c := &inProgressCompactions[i]
// If this compaction's version edit has already been applied, there's
// no need to adjust: The LSM we'll examine will already reflect the
// new LSM state.
if c.versionEditApplied {
continue
}
for _, input := range c.inputs { for _, input := range c.inputs {
real := int64(input.files.SizeSum()) real := int64(input.files.SizeSum())
compensated := int64(totalCompensatedSize(input.files.Iter(), pointTombstoneWeight)) compensated := int64(totalCompensatedSize(input.files.Iter()))
if input.level != c.outputLevel { if input.level != c.outputLevel {
sizeAdjust[input.level] -= compensated sizeAdjust[input.level] -= compensated
@@ -967,10 +971,7 @@ func (p *compactionPickerByScore) calculateScores(
} }
scores[0] = p.calculateL0Score(inProgressCompactions) scores[0] = p.calculateL0Score(inProgressCompactions)
sizeAdjust := calculateSizeAdjust( sizeAdjust := calculateSizeAdjust(inProgressCompactions)
inProgressCompactions,
p.opts.Experimental.PointTombstoneWeight,
)
for level := 1; level < numLevels; level++ { for level := 1; level < numLevels; level++ {
levelSize := int64(levelCompensatedSize(p.vers.Levels[level])) + sizeAdjust[level] levelSize := int64(levelCompensatedSize(p.vers.Levels[level])) + sizeAdjust[level]
scores[level].score = float64(levelSize) / float64(p.levelMaxBytes[level]) scores[level].score = float64(levelSize) / float64(p.levelMaxBytes[level])
@@ -1088,14 +1089,20 @@ func (p *compactionPickerByScore) pickFile(
for f := startIter.First(); f != nil; f = startIter.Next() { for f := startIter.First(); f != nil; f = startIter.Next() {
var overlappingBytes uint64 var overlappingBytes uint64
compacting := f.IsCompacting()
if compacting {
// Move on if this file is already being compacted. We'll likely
// still need to move past the overlapping output files regardless,
// but in cases where all start-level files are compacting we won't.
continue
}
// Trim any output-level files smaller than f. // Trim any output-level files smaller than f.
for outputFile != nil && base.InternalCompare(cmp, outputFile.Largest, f.Smallest) < 0 { for outputFile != nil && sstableKeyCompare(cmp, outputFile.Largest, f.Smallest) < 0 {
outputFile = outputIter.Next() outputFile = outputIter.Next()
} }
compacting := f.IsCompacting() for outputFile != nil && sstableKeyCompare(cmp, outputFile.Smallest, f.Largest) <= 0 && !compacting {
for outputFile != nil && base.InternalCompare(cmp, outputFile.Smallest, f.Largest) < 0 {
overlappingBytes += outputFile.Size overlappingBytes += outputFile.Size
compacting = compacting || outputFile.IsCompacting() compacting = compacting || outputFile.IsCompacting()
@@ -1114,7 +1121,32 @@ func (p *compactionPickerByScore) pickFile(
// If the file in the next level extends beyond f's largest key, // If the file in the next level extends beyond f's largest key,
// break out and don't advance outputIter because f's successor // break out and don't advance outputIter because f's successor
// might also overlap. // might also overlap.
if base.InternalCompare(cmp, outputFile.Largest, f.Largest) > 0 { //
// Note, we stop as soon as we encounter an output-level file with a
// largest key beyond the input-level file's largest bound. We
// perform a simple user key comparison here using sstableKeyCompare
// which handles the potential for exclusive largest key bounds.
// There's some subtlety when the bounds are equal (eg, equal and
// inclusive, or equal and exclusive). Current Pebble doesn't split
// user keys across sstables within a level (and in format versions
// FormatSplitUserKeysMarkedCompacted and later we guarantee no
// split user keys exist within the entire LSM). In that case, we're
// assured that neither the input level nor the output level's next
// file shares the same user key, so compaction expansion will not
// include them in any compaction compacting `f`.
//
// NB: If we /did/ allow split user keys, or we're running on an
// old database with an earlier format major version where there are
// existing split user keys, this logic would be incorrect. Consider
// L1: [a#120,a#100] [a#80,a#60]
// L2: [a#55,a#45] [a#35,a#25] [a#15,a#5]
// While considering the first file in L1, [a#120,a#100], we'd skip
// past all of the files in L2. When considering the second file in
// L1, we'd improperly conclude that the second file overlaps
// nothing in the second level and is cheap to compact, when in
// reality we'd need to expand the compaction to include all 5
// files.
if sstableKeyCompare(cmp, outputFile.Largest, f.Largest) > 0 {
break break
} }
outputFile = outputIter.Next() outputFile = outputIter.Next()
@@ -1127,9 +1159,9 @@ func (p *compactionPickerByScore) pickFile(
continue continue
} }
compSz := compensatedSize(f, p.opts.Experimental.PointTombstoneWeight) compSz := compensatedSize(f)
scaledRatio := overlappingBytes * 1024 / compSz scaledRatio := overlappingBytes * 1024 / compSz
if scaledRatio < smallestRatio && !f.IsCompacting() { if scaledRatio < smallestRatio {
smallestRatio = scaledRatio smallestRatio = scaledRatio
file = startIter.Take() file = startIter.Take()
} }
@@ -1191,7 +1223,7 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact
fmt.Fprintf(&buf, " %sL%d: %5.1f %5.1f %8s %8s", fmt.Fprintf(&buf, " %sL%d: %5.1f %5.1f %8s %8s",
marker, info.level, info.score, info.origScore, marker, info.level, info.score, info.origScore,
humanize.Int64(int64(totalCompensatedSize( humanize.Int64(int64(totalCompensatedSize(
p.vers.Levels[info.level].Iter(), p.opts.Experimental.PointTombstoneWeight, p.vers.Levels[info.level].Iter(),
))), ))),
humanize.Int64(p.levelMaxBytes[info.level]), humanize.Int64(p.levelMaxBytes[info.level]),
) )

View File

@@ -96,6 +96,22 @@ type Writer interface {
// It is safe to modify the contents of the arguments after Delete returns. // It is safe to modify the contents of the arguments after Delete returns.
Delete(key []byte, o *WriteOptions) error Delete(key []byte, o *WriteOptions) error
// DeleteSized behaves identically to Delete, but takes an additional
// argument indicating the size of the value being deleted. DeleteSized
// should be preferred when the caller has the expectation that there exists
// a single internal KV pair for the key (eg, the key has not been
// overwritten recently), and the caller knows the size of its value.
//
// DeleteSized will record the value size within the tombstone and use it to
// inform compaction-picking heuristics which strive to reduce space
// amplification in the LSM. This "calling your shot" mechanic allows the
// storage engine to more accurately estimate and reduce space
// amplification.
//
// It is safe to modify the contents of the arguments after DeleteSized
// returns.
DeleteSized(key []byte, valueSize uint32, _ *WriteOptions) error
// SingleDelete is similar to Delete in that it deletes the value for the given key. Like Delete, // SingleDelete is similar to Delete in that it deletes the value for the given key. Like Delete,
// it is a blind operation that will succeed even if the given key does not exist. // it is a blind operation that will succeed even if the given key does not exist.
// //
@@ -620,6 +636,30 @@ func (d *DB) Delete(key []byte, opts *WriteOptions) error {
return nil return nil
} }
// DeleteSized behaves identically to Delete, but takes an additional
// argument indicating the size of the value being deleted. DeleteSized
// should be preferred when the caller has the expectation that there exists
// a single internal KV pair for the key (eg, the key has not been
// overwritten recently), and the caller knows the size of its value.
//
// DeleteSized will record the value size within the tombstone and use it to
// inform compaction-picking heuristics which strive to reduce space
// amplification in the LSM. This "calling your shot" mechanic allows the
// storage engine to more accurately estimate and reduce space amplification.
//
// It is safe to modify the contents of the arguments after DeleteSized
// returns.
func (d *DB) DeleteSized(key []byte, valueSize uint32, opts *WriteOptions) error {
b := newBatch(d)
_ = b.DeleteSized(key, valueSize, opts)
if err := d.Apply(b, opts); err != nil {
return err
}
// Only release the batch on success.
b.release()
return nil
}
// SingleDelete adds an action to the batch that single deletes the entry for key. // SingleDelete adds an action to the batch that single deletes the entry for key.
// See Writer.SingleDelete for more details on the semantics of SingleDelete. // See Writer.SingleDelete for more details on the semantics of SingleDelete.
// //
@@ -786,17 +826,19 @@ func (d *DB) applyInternal(batch *Batch, opts *WriteOptions, noSyncWait bool) er
return errors.New("pebble: WAL disabled") return errors.New("pebble: WAL disabled")
} }
if batch.minimumFormatMajorVersion != FormatMostCompatible {
if fmv := d.FormatMajorVersion(); fmv < batch.minimumFormatMajorVersion {
panic(fmt.Sprintf(
"pebble: batch requires at least format major version %d (current: %d)",
batch.minimumFormatMajorVersion, fmv,
))
}
}
if batch.countRangeKeys > 0 { if batch.countRangeKeys > 0 {
if d.split == nil { if d.split == nil {
return errNoSplit return errNoSplit
} }
if d.FormatMajorVersion() < FormatRangeKeys {
panic(fmt.Sprintf(
"pebble: range keys require at least format major version %d (current: %d)",
FormatRangeKeys, d.FormatMajorVersion(),
))
}
// TODO(jackson): Assert that all range key operands are suffixless. // TODO(jackson): Assert that all range key operands are suffixless.
} }
@@ -1320,6 +1362,7 @@ func (i *Iterator) constructPointIter(
levelsIndex := len(levels) levelsIndex := len(levels)
mlevels = mlevels[:numMergingLevels] mlevels = mlevels[:numMergingLevels]
levels = levels[:numLevelIters] levels = levels[:numLevelIters]
i.opts.snapshotForHideObsoletePoints = buf.dbi.seqNum
addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) { addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) {
li := &levels[levelsIndex] li := &levels[levelsIndex]
@@ -1523,6 +1566,16 @@ func (d *DB) Close() error {
d.mu.Unlock() d.mu.Unlock()
d.deleters.Wait() d.deleters.Wait()
d.compactionSchedulers.Wait() d.compactionSchedulers.Wait()
// Sanity check metrics.
if invariants.Enabled {
m := d.Metrics()
if m.Compact.NumInProgress > 0 || m.Compact.InProgressBytes > 0 {
d.mu.Lock()
panic(fmt.Sprintf("invalid metrics on close:\n%s", m))
}
}
d.mu.Lock() d.mu.Lock()
// As a sanity check, ensure that there are no zombie tables. A non-zero count // As a sanity check, ensure that there are no zombie tables. A non-zero count
@@ -1823,6 +1876,10 @@ func (d *DB) Metrics() *Metrics {
type sstablesOptions struct { type sstablesOptions struct {
// set to true will return the sstable properties in TableInfo // set to true will return the sstable properties in TableInfo
withProperties bool withProperties bool
// if set, return sstables that overlap the key range (end-exclusive)
start []byte
end []byte
} }
// SSTablesOption set optional parameter used by `DB.SSTables`. // SSTablesOption set optional parameter used by `DB.SSTables`.
@@ -1838,6 +1895,15 @@ func WithProperties() SSTablesOption {
} }
} }
// WithKeyRangeFilter ensures returned sstables overlap start and end (end-exclusive)
// if start and end are both nil these properties have no effect
func WithKeyRangeFilter(start, end []byte) SSTablesOption {
return func(opt *sstablesOptions) {
opt.end = end
opt.start = start
}
}
// SSTableInfo export manifest.TableInfo with sstable.Properties // SSTableInfo export manifest.TableInfo with sstable.Properties
type SSTableInfo struct { type SSTableInfo struct {
manifest.TableInfo manifest.TableInfo
@@ -1883,6 +1949,9 @@ func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) {
iter := srcLevels[i].Iter() iter := srcLevels[i].Iter()
j := 0 j := 0
for m := iter.First(); m != nil; m = iter.Next() { for m := iter.First(); m != nil; m = iter.Next() {
if opt.start != nil && opt.end != nil && !m.Overlaps(d.opts.Comparer.Compare, opt.start, opt.end, true /* exclusive end */) {
continue
}
destTables[j] = SSTableInfo{TableInfo: m.TableInfo()} destTables[j] = SSTableInfo{TableInfo: m.TableInfo()}
if opt.withProperties { if opt.withProperties {
p, err := d.tableCache.getTableProperties( p, err := d.tableCache.getTableProperties(
@@ -1900,6 +1969,7 @@ func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) {
destLevels[i] = destTables[:j] destLevels[i] = destTables[:j]
destTables = destTables[j:] destTables = destTables[j:]
} }
return destLevels, nil return destLevels, nil
} }

View File

@@ -209,15 +209,15 @@ func createExternalPointIter(ctx context.Context, it *Iterator) (internalIterato
pointIter internalIterator pointIter internalIterator
err error err error
) )
pointIter, err = r.NewIterWithBlockPropertyFiltersAndContext( // We could set hideObsoletePoints=true, since we are reading at
ctx, // InternalKeySeqNumMax, but we don't bother since these sstables should
it.opts.LowerBound, // not have obsolete points (so the performance optimization is
it.opts.UpperBound, // unnecessary), and we don't want to bother constructing a
nil, /* BlockPropertiesFilterer */ // BlockPropertiesFilterer that includes obsoleteKeyBlockPropertyFilter.
false, /* useFilterBlock */ pointIter, err = r.NewIterWithBlockPropertyFiltersAndContextEtc(
&it.stats.InternalStats, ctx, it.opts.LowerBound, it.opts.UpperBound, nil, /* BlockPropertiesFilterer */
sstable.TrivialReaderProvider{Reader: r}, false /* hideObsoletePoints */, false, /* useFilterBlock */
) &it.stats.InternalStats, sstable.TrivialReaderProvider{Reader: r})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -184,7 +184,7 @@ func (s *ingestedFlushable) newFlushIter(o *IterOptions, bytesFlushed *uint64) i
} }
func (s *ingestedFlushable) constructRangeDelIter( func (s *ingestedFlushable) constructRangeDelIter(
file *manifest.FileMetadata, _ *keyspan.SpanIterOptions, file *manifest.FileMetadata, _ keyspan.SpanIterOptions,
) (keyspan.FragmentIterator, error) { ) (keyspan.FragmentIterator, error) {
// Note that the keyspan level iter expects a non-nil iterator to be // Note that the keyspan level iter expects a non-nil iterator to be
// returned even if there is an error. So, we return the emptyKeyspanIter. // returned even if there is an error. So, we return the emptyKeyspanIter.

View File

@@ -151,8 +151,29 @@ const (
// compactions for files marked for compaction are complete. // compactions for files marked for compaction are complete.
FormatPrePebblev1MarkedCompacted FormatPrePebblev1MarkedCompacted
// ExperimentalFormatDeleteSizedAndObsolete is a format major version that adds support
// for deletion tombstones that encode the size of the value they're
// expected to delete. This format major version is required before the
// associated key kind may be committed through batch applications or
// ingests. It also adds support for keys that are marked obsolete (see
// sstable/format.go for details).
ExperimentalFormatDeleteSizedAndObsolete
// ExperimentalFormatVirtualSSTables is a format major version that adds support for
// virtual sstables that can reference a sub-range of keys in an underlying
// physical sstable. This information is persisted through new,
// backward-incompatible fields in the Manifest, and therefore requires
// a format major version.
ExperimentalFormatVirtualSSTables
// internalFormatNewest holds the newest format major version, including
// experimental ones excluded from the exported FormatNewest constant until
// they've stabilized. Used in tests.
internalFormatNewest FormatMajorVersion = iota - 1
// FormatNewest always contains the most recent format major version. // FormatNewest always contains the most recent format major version.
FormatNewest FormatMajorVersion = iota - 1 // TODO(jackson): restore FormatNewest to internalFormatNewest.
FormatNewest FormatMajorVersion = FormatPrePebblev1MarkedCompacted
) )
// MaxTableFormat returns the maximum sstable.TableFormat that can be used at // MaxTableFormat returns the maximum sstable.TableFormat that can be used at
@@ -170,6 +191,8 @@ func (v FormatMajorVersion) MaxTableFormat() sstable.TableFormat {
return sstable.TableFormatPebblev2 return sstable.TableFormatPebblev2
case FormatSSTableValueBlocks, FormatFlushableIngest, FormatPrePebblev1MarkedCompacted: case FormatSSTableValueBlocks, FormatFlushableIngest, FormatPrePebblev1MarkedCompacted:
return sstable.TableFormatPebblev3 return sstable.TableFormatPebblev3
case ExperimentalFormatDeleteSizedAndObsolete, ExperimentalFormatVirtualSSTables:
return sstable.TableFormatPebblev4
default: default:
panic(fmt.Sprintf("pebble: unsupported format major version: %s", v)) panic(fmt.Sprintf("pebble: unsupported format major version: %s", v))
} }
@@ -186,7 +209,8 @@ func (v FormatMajorVersion) MinTableFormat() sstable.TableFormat {
return sstable.TableFormatLevelDB return sstable.TableFormatLevelDB
case FormatMinTableFormatPebblev1, FormatPrePebblev1Marked, case FormatMinTableFormatPebblev1, FormatPrePebblev1Marked,
FormatUnusedPrePebblev1MarkedCompacted, FormatSSTableValueBlocks, FormatUnusedPrePebblev1MarkedCompacted, FormatSSTableValueBlocks,
FormatFlushableIngest, FormatPrePebblev1MarkedCompacted: FormatFlushableIngest, FormatPrePebblev1MarkedCompacted,
ExperimentalFormatDeleteSizedAndObsolete, ExperimentalFormatVirtualSSTables:
return sstable.TableFormatPebblev1 return sstable.TableFormatPebblev1
default: default:
panic(fmt.Sprintf("pebble: unsupported format major version: %s", v)) panic(fmt.Sprintf("pebble: unsupported format major version: %s", v))
@@ -319,6 +343,12 @@ var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{
} }
return d.finalizeFormatVersUpgrade(FormatPrePebblev1MarkedCompacted) return d.finalizeFormatVersUpgrade(FormatPrePebblev1MarkedCompacted)
}, },
ExperimentalFormatDeleteSizedAndObsolete: func(d *DB) error {
return d.finalizeFormatVersUpgrade(ExperimentalFormatDeleteSizedAndObsolete)
},
ExperimentalFormatVirtualSSTables: func(d *DB) error {
return d.finalizeFormatVersUpgrade(ExperimentalFormatVirtualSSTables)
},
} }
const formatVersionMarkerName = `format-version` const formatVersionMarkerName = `format-version`
@@ -341,7 +371,7 @@ func lookupFormatMajorVersion(
if vers == FormatDefault { if vers == FormatDefault {
return 0, nil, errors.Newf("pebble: default format major version should not persisted", vers) return 0, nil, errors.Newf("pebble: default format major version should not persisted", vers)
} }
if vers > FormatNewest { if vers > internalFormatNewest {
return 0, nil, errors.Newf("pebble: database %q written in format major version %d", dirname, vers) return 0, nil, errors.Newf("pebble: database %q written in format major version %d", dirname, vers)
} }
return vers, m, nil return vers, m, nil
@@ -377,8 +407,8 @@ func (d *DB) ratchetFormatMajorVersionLocked(formatVers FormatMajorVersion) erro
if d.opts.ReadOnly { if d.opts.ReadOnly {
return ErrReadOnly return ErrReadOnly
} }
if formatVers > FormatNewest { if formatVers > internalFormatNewest {
// Guard against accidentally forgetting to update FormatNewest. // Guard against accidentally forgetting to update internalFormatNewest.
return errors.Errorf("pebble: unknown format version %d", formatVers) return errors.Errorf("pebble: unknown format version %d", formatVers)
} }
if d.mu.formatVers.vers > formatVers { if d.mu.formatVers.vers > formatVers {

View File

@@ -158,7 +158,7 @@ func (g *getIter) Next() (*InternalKey, base.LazyValue) {
if n := len(g.l0); n > 0 { if n := len(g.l0); n > 0 {
files := g.l0[n-1].Iter() files := g.l0[n-1].Iter()
g.l0 = g.l0[:n-1] g.l0 = g.l0[:n-1]
iterOpts := IterOptions{logger: g.logger} iterOpts := IterOptions{logger: g.logger, snapshotForHideObsoletePoints: g.snapshot}
g.levelIter.init(context.Background(), iterOpts, g.cmp, nil /* split */, g.newIters, g.levelIter.init(context.Background(), iterOpts, g.cmp, nil /* split */, g.newIters,
files, manifest.L0Sublevel(n), internalIterOpts{}) files, manifest.L0Sublevel(n), internalIterOpts{})
g.levelIter.initRangeDel(&g.rangeDelIter) g.levelIter.initRangeDel(&g.rangeDelIter)
@@ -177,7 +177,7 @@ func (g *getIter) Next() (*InternalKey, base.LazyValue) {
continue continue
} }
iterOpts := IterOptions{logger: g.logger} iterOpts := IterOptions{logger: g.logger, snapshotForHideObsoletePoints: g.snapshot}
g.levelIter.init(context.Background(), iterOpts, g.cmp, nil /* split */, g.newIters, g.levelIter.init(context.Background(), iterOpts, g.cmp, nil /* split */, g.newIters,
g.version.Levels[g.level].Iter(), manifest.Level(g.level), internalIterOpts{}) g.version.Levels[g.level].Iter(), manifest.Level(g.level), internalIterOpts{})
g.levelIter.initRangeDel(&g.rangeDelIter) g.levelIter.initRangeDel(&g.rangeDelIter)

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ const (
InternalKeyKindRangeKeyUnset = base.InternalKeyKindRangeKeyUnset InternalKeyKindRangeKeyUnset = base.InternalKeyKindRangeKeyUnset
InternalKeyKindRangeKeyDelete = base.InternalKeyKindRangeKeyDelete InternalKeyKindRangeKeyDelete = base.InternalKeyKindRangeKeyDelete
InternalKeyKindIngestSST = base.InternalKeyKindIngestSST InternalKeyKindIngestSST = base.InternalKeyKindIngestSST
InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized
InternalKeyKindInvalid = base.InternalKeyKindInvalid InternalKeyKindInvalid = base.InternalKeyKindInvalid
InternalKeySeqNumBatch = base.InternalKeySeqNumBatch InternalKeySeqNumBatch = base.InternalKeySeqNumBatch
InternalKeySeqNumMax = base.InternalKeySeqNumMax InternalKeySeqNumMax = base.InternalKeySeqNumMax

View File

@@ -24,6 +24,14 @@ const (
//InternalKeyKindColumnFamilyDeletion InternalKeyKind = 4 //InternalKeyKindColumnFamilyDeletion InternalKeyKind = 4
//InternalKeyKindColumnFamilyValue InternalKeyKind = 5 //InternalKeyKindColumnFamilyValue InternalKeyKind = 5
//InternalKeyKindColumnFamilyMerge InternalKeyKind = 6 //InternalKeyKindColumnFamilyMerge InternalKeyKind = 6
// InternalKeyKindSingleDelete (SINGLEDEL) is a performance optimization
// solely for compactions (to reduce write amp and space amp). Readers other
// than compactions should treat SINGLEDEL as equivalent to a DEL.
// Historically, it was simpler for readers other than compactions to treat
// SINGLEDEL as equivalent to DEL, but as of the introduction of
// InternalKeyKindSSTableInternalObsoleteBit, this is also necessary for
// correctness.
InternalKeyKindSingleDelete InternalKeyKind = 7 InternalKeyKindSingleDelete InternalKeyKind = 7
//InternalKeyKindColumnFamilySingleDelete InternalKeyKind = 8 //InternalKeyKindColumnFamilySingleDelete InternalKeyKind = 8
//InternalKeyKindBeginPrepareXID InternalKeyKind = 9 //InternalKeyKindBeginPrepareXID InternalKeyKind = 9
@@ -66,8 +74,15 @@ const (
// batch, or in an sstable. // batch, or in an sstable.
InternalKeyKindIngestSST InternalKeyKind = 22 InternalKeyKindIngestSST InternalKeyKind = 22
// This maximum value isn't part of the file format. It's unlikely, // InternalKeyKindDeleteSized keys behave identically to
// but future extensions may increase this value. // InternalKeyKindDelete keys, except that they hold an associated uint64
// value indicating the (len(key)+len(value)) of the shadowed entry the
// tombstone is expected to delete. This value is used to inform compaction
// heuristics, but is not required to be accurate for correctness.
InternalKeyKindDeleteSized InternalKeyKind = 23
// This maximum value isn't part of the file format. Future extensions may
// increase this value.
// //
// When constructing an internal key to pass to DB.Seek{GE,LE}, // When constructing an internal key to pass to DB.Seek{GE,LE},
// internalKeyComparer sorts decreasing by kind (after sorting increasing by // internalKeyComparer sorts decreasing by kind (after sorting increasing by
@@ -75,14 +90,19 @@ const (
// which sorts 'less than or equal to' any other valid internalKeyKind, when // which sorts 'less than or equal to' any other valid internalKeyKind, when
// searching for any kind of internal key formed by a certain user key and // searching for any kind of internal key formed by a certain user key and
// seqNum. // seqNum.
InternalKeyKindMax InternalKeyKind = 22 InternalKeyKindMax InternalKeyKind = 23
// Internal to the sstable format. Not exposed by any sstable iterator.
// Declared here to prevent definition of valid key kinds that set this bit.
InternalKeyKindSSTableInternalObsoleteBit InternalKeyKind = 64
InternalKeyKindSSTableInternalObsoleteMask InternalKeyKind = 191
// InternalKeyZeroSeqnumMaxTrailer is the largest trailer with a // InternalKeyZeroSeqnumMaxTrailer is the largest trailer with a
// zero sequence number. // zero sequence number.
InternalKeyZeroSeqnumMaxTrailer = uint64(InternalKeyKindInvalid) InternalKeyZeroSeqnumMaxTrailer = uint64(255)
// A marker for an invalid key. // A marker for an invalid key.
InternalKeyKindInvalid InternalKeyKind = 255 InternalKeyKindInvalid InternalKeyKind = InternalKeyKindSSTableInternalObsoleteMask
// InternalKeySeqNumBatch is a bit that is set on batch sequence numbers // InternalKeySeqNumBatch is a bit that is set on batch sequence numbers
// which prevents those entries from being excluded from iteration. // which prevents those entries from being excluded from iteration.
@@ -105,6 +125,9 @@ const (
InternalKeyBoundaryRangeKey = (InternalKeySeqNumMax << 8) | uint64(InternalKeyKindRangeKeySet) InternalKeyBoundaryRangeKey = (InternalKeySeqNumMax << 8) | uint64(InternalKeyKindRangeKeySet)
) )
// Assert InternalKeyKindSSTableInternalObsoleteBit > InternalKeyKindMax
const _ = uint(InternalKeyKindSSTableInternalObsoleteBit - InternalKeyKindMax - 1)
var internalKeyKindNames = []string{ var internalKeyKindNames = []string{
InternalKeyKindDelete: "DEL", InternalKeyKindDelete: "DEL",
InternalKeyKindSet: "SET", InternalKeyKindSet: "SET",
@@ -118,6 +141,7 @@ var internalKeyKindNames = []string{
InternalKeyKindRangeKeyUnset: "RANGEKEYUNSET", InternalKeyKindRangeKeyUnset: "RANGEKEYUNSET",
InternalKeyKindRangeKeyDelete: "RANGEKEYDEL", InternalKeyKindRangeKeyDelete: "RANGEKEYDEL",
InternalKeyKindIngestSST: "INGESTSST", InternalKeyKindIngestSST: "INGESTSST",
InternalKeyKindDeleteSized: "DELSIZED",
InternalKeyKindInvalid: "INVALID", InternalKeyKindInvalid: "INVALID",
} }
@@ -204,6 +228,7 @@ var kindsMap = map[string]InternalKeyKind{
"RANGEKEYUNSET": InternalKeyKindRangeKeyUnset, "RANGEKEYUNSET": InternalKeyKindRangeKeyUnset,
"RANGEKEYDEL": InternalKeyKindRangeKeyDelete, "RANGEKEYDEL": InternalKeyKindRangeKeyDelete,
"INGESTSST": InternalKeyKindIngestSST, "INGESTSST": InternalKeyKindIngestSST,
"DELSIZED": InternalKeyKindDeleteSized,
} }
// ParseInternalKey parses the string representation of an internal key. The // ParseInternalKey parses the string representation of an internal key. The

View File

@@ -53,6 +53,15 @@ func (tm *ThroughputMetric) Rate() int64 {
float64(time.Second)) float64(time.Second))
} }
// Utilization returns a percent [0, 1.0] indicating the percent of time
// work was performed.
func (tm *ThroughputMetric) Utilization() float64 {
if tm.WorkDuration == 0 {
return 0
}
return float64(tm.WorkDuration) / float64(tm.WorkDuration+tm.IdleDuration)
}
// GaugeSampleMetric is used to measure a gauge value (e.g. queue length) by // GaugeSampleMetric is used to measure a gauge value (e.g. queue length) by
// accumulating samples of that gauge. // accumulating samples of that gauge.
type GaugeSampleMetric struct { type GaugeSampleMetric struct {

View File

@@ -4,6 +4,8 @@
package base package base
import "fmt"
// This file defines sequence numbers that are reserved for foreign keys i.e. // This file defines sequence numbers that are reserved for foreign keys i.e.
// internal keys coming from other Pebble instances and existing in shared // internal keys coming from other Pebble instances and existing in shared
// storage, as those will "slot below" any internal keys added by our own Pebble // storage, as those will "slot below" any internal keys added by our own Pebble
@@ -14,41 +16,41 @@ const (
// guarantee there are no keys underneath an internal key. // guarantee there are no keys underneath an internal key.
SeqNumZero = uint64(0) SeqNumZero = uint64(0)
// SeqNumL6Point is the sequence number reserved for foreign point keys in L6. // SeqNumL6 is the sequence number reserved for foreign keys in L6. This seqnum
// This sequence number must be lower than SeqNumL6RangeKey for range key // will be used to expose any range key sets as well as point keys in L6. Range
// masking to work correctly. // deletes do not need to be exposed in L6.
SeqNumL6Point = uint64(1) SeqNumL6 = uint64(1)
// SeqNumL6RangeKey is the sequence number reserved for foreign range keys in // SeqNumL5 is the sequence number reserved for foreign keys in L5. This seqnum
// L6. Only RangeKeySets are expected at this level. // needs to be greater than SeqNumL6, so that range deletes in L5 can be
SeqNumL6RangeKey = uint64(2) // exposed at this level and will correctly delete covering points in L6. Also
// range key unsets/dels will be exposed at this seqnum and will need to shadow
// overlapping range keys in L6.
//
// Note that we can use the same sequence number for all exposed keys in L5 as
// range dels do not delete points at the same seqnum, and range key
// unsets/deletes do not coalesce with range key sets at the same seqnum. Range
// key masking does not care about the sequence number of overlapping points
// (rather, it applies to points based on suffixes), so we can use this seqnum
// for all L5 keys exposed.
SeqNumL5 = uint64(2)
// SeqNumL5RangeDel is the sequence number reserved for foreign range deletes // Sequence numbers 3-9 are reserved for future use.
// in L5. These keys could delete L6 points.
SeqNumL5RangeDel = uint64(3)
// SeqNumL5Point is the sequence number reserved for foreign point keys in L5.
// Any sst-local range deletions would have already been applied to these keys,
// so they can safely get a sequence number higher than SeqNumL5RangeDel.
// However they must have a sequence number lower than SeqNumL5RangeKey* for
// range key masking to work correctly.
SeqNumL5Point = uint64(4)
// SeqNumL5RangeKeyUnsetDel is the sequence number reserved for foreign
// range key unsets/deletes in L5. These operations could apply to L6
// RangeKeySets, so this sequence number must be > SeqNumL6RangeKey.
SeqNumL5RangeKeyUnsetDel = uint64(5)
// SeqNumL5RangeKeySet is the sequence number reserved for foreign range key
// Sets in L5. These operations could apply to L6 RangeKeySets, so this
// sequence number must be > SeqNumL6RangeKey. Any SST-local rangekey
// unsets/dels have already been applied to them, so their sequence number must
// be > SeqNumL5RangeKeyUnsetDel.
SeqNumL5RangeKeySet = uint64(6)
// Sequence numbers 7-9 are reserved for future use.
// SeqNumStart is the first sequence number assigned to a key written by // SeqNumStart is the first sequence number assigned to a key written by
// ourselves. // ourselves.
SeqNumStart = uint64(10) SeqNumStart = uint64(10)
) )
// SeqNumForLevel returns the appropriate reserved sequence number for keys in
// foreign sstables at the specified level.
func SeqNumForLevel(level int) uint64 {
switch level {
case 5:
return SeqNumL5
case 6:
return SeqNumL6
default:
panic(fmt.Sprintf("unexpected foreign sstable at level %d", level))
}
}

View File

@@ -62,7 +62,7 @@ type FragmentIterator interface {
// TableNewSpanIter creates a new iterator for range key spans for the given // TableNewSpanIter creates a new iterator for range key spans for the given
// file. // file.
type TableNewSpanIter func(file *manifest.FileMetadata, iterOptions *SpanIterOptions) (FragmentIterator, error) type TableNewSpanIter func(file *manifest.FileMetadata, iterOptions SpanIterOptions) (FragmentIterator, error)
// SpanIterOptions is a subset of IterOptions that are necessary to instantiate // SpanIterOptions is a subset of IterOptions that are necessary to instantiate
// per-sstable span iterators. // per-sstable span iterators.
@@ -70,6 +70,9 @@ type SpanIterOptions struct {
// RangeKeyFilters can be used to avoid scanning tables and blocks in tables // RangeKeyFilters can be used to avoid scanning tables and blocks in tables
// when iterating over range keys. // when iterating over range keys.
RangeKeyFilters []base.BlockPropertyFilter RangeKeyFilters []base.BlockPropertyFilter
// Level specifies the level where this sstable is being read. Must be
// specified for foreign (i.e. shared not-created-by-this-instance) sstables.
Level manifest.Level
} }
// Iter is an iterator over a set of fragmented spans. // Iter is an iterator over a set of fragmented spans.

View File

@@ -98,7 +98,7 @@ func (l *LevelIter) Init(
) { ) {
l.err = nil l.err = nil
l.level = level l.level = level
l.tableOpts.RangeKeyFilters = opts.RangeKeyFilters l.tableOpts = opts
l.cmp = cmp l.cmp = cmp
l.iterFile = nil l.iterFile = nil
l.newIter = newIter l.newIter = newIter
@@ -179,7 +179,7 @@ func (l *LevelIter) loadFile(file *manifest.FileMetadata, dir int) loadFileRetur
return noFileLoaded return noFileLoaded
} }
if indicator != fileAlreadyLoaded { if indicator != fileAlreadyLoaded {
l.iter, l.err = l.newIter(file, &l.tableOpts) l.iter, l.err = l.newIter(file, l.tableOpts)
indicator = newFileLoaded indicator = newFileLoaded
} }
if l.err != nil { if l.err != nil {

View File

@@ -25,49 +25,6 @@ import (
// MergingIter implementation, but will require a bit of plumbing to thread the // MergingIter implementation, but will require a bit of plumbing to thread the
// Equal function. // Equal function.
// Transformer defines a transformation to be applied to a Span.
type Transformer interface {
// Transform takes a Span as input and writes the transformed Span to the
// provided output *Span pointer. The output Span's Keys slice may be reused
// by Transform to reduce allocations.
Transform(cmp base.Compare, in Span, out *Span) error
}
// The TransformerFunc type is an adapter to allow the use of ordinary functions
// as Transformers. If f is a function with the appropriate signature,
// TransformerFunc(f) is a Transformer that calls f.
type TransformerFunc func(base.Compare, Span, *Span) error
// Transform calls f(cmp, in, out).
func (tf TransformerFunc) Transform(cmp base.Compare, in Span, out *Span) error {
return tf(cmp, in, out)
}
var noopTransform Transformer = TransformerFunc(func(_ base.Compare, s Span, dst *Span) error {
dst.Start, dst.End = s.Start, s.End
dst.Keys = append(dst.Keys[:0], s.Keys...)
return nil
})
// VisibleTransform filters keys that are invisible at the provided snapshot
// sequence number.
func VisibleTransform(snapshot uint64) Transformer {
return TransformerFunc(func(_ base.Compare, s Span, dst *Span) error {
dst.Start, dst.End = s.Start, s.End
dst.Keys = dst.Keys[:0]
for _, k := range s.Keys {
// NB: The InternalKeySeqNumMax value is used for the batch snapshot
// because a batch's visible span keys are filtered when they're
// fragmented. There's no requirement to enforce visibility at
// iteration time.
if base.Visible(k.SeqNum(), snapshot, base.InternalKeySeqNumMax) {
dst.Keys = append(dst.Keys, k)
}
}
return nil
})
}
// MergingIter merges spans across levels of the LSM, exposing an iterator over // MergingIter merges spans across levels of the LSM, exposing an iterator over
// spans that yields sets of spans fragmented at unique user key boundaries. // spans that yields sets of spans fragmented at unique user key boundaries.
// //

View File

@@ -368,6 +368,7 @@ func (s Span) String() string {
// Pretty returns a formatter for the span. // Pretty returns a formatter for the span.
func (s Span) Pretty(f base.FormatKey) fmt.Formatter { func (s Span) Pretty(f base.FormatKey) fmt.Formatter {
// TODO(jackson): Take a base.FormatValue to format Key.Value too.
return prettySpan{s, f} return prettySpan{s, f}
} }

View File

@@ -0,0 +1,149 @@
// Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package keyspan
import "github.com/cockroachdb/pebble/internal/base"
// Transformer defines a transformation to be applied to a Span.
type Transformer interface {
// Transform takes a Span as input and writes the transformed Span to the
// provided output *Span pointer. The output Span's Keys slice may be reused
// by Transform to reduce allocations.
Transform(cmp base.Compare, in Span, out *Span) error
}
// The TransformerFunc type is an adapter to allow the use of ordinary functions
// as Transformers. If f is a function with the appropriate signature,
// TransformerFunc(f) is a Transformer that calls f.
type TransformerFunc func(base.Compare, Span, *Span) error
// Transform calls f(cmp, in, out).
func (tf TransformerFunc) Transform(cmp base.Compare, in Span, out *Span) error {
return tf(cmp, in, out)
}
var noopTransform Transformer = TransformerFunc(func(_ base.Compare, s Span, dst *Span) error {
dst.Start, dst.End = s.Start, s.End
dst.Keys = append(dst.Keys[:0], s.Keys...)
return nil
})
// VisibleTransform filters keys that are invisible at the provided snapshot
// sequence number.
func VisibleTransform(snapshot uint64) Transformer {
return TransformerFunc(func(_ base.Compare, s Span, dst *Span) error {
dst.Start, dst.End = s.Start, s.End
dst.Keys = dst.Keys[:0]
for _, k := range s.Keys {
// NB: The InternalKeySeqNumMax value is used for the batch snapshot
// because a batch's visible span keys are filtered when they're
// fragmented. There's no requirement to enforce visibility at
// iteration time.
if base.Visible(k.SeqNum(), snapshot, base.InternalKeySeqNumMax) {
dst.Keys = append(dst.Keys, k)
}
}
return nil
})
}
// TransformerIter is a FragmentIterator that applies a Transformer on all
// returned keys. Used for when a caller needs to apply a transformer on an
// iterator but does not otherwise need the mergingiter's merging ability.
type TransformerIter struct {
FragmentIterator
// Transformer is applied on every Span returned by this iterator.
Transformer Transformer
// Comparer in use for this keyspace.
Compare base.Compare
span Span
err error
}
func (t *TransformerIter) applyTransform(span *Span) *Span {
t.span = Span{
Start: t.span.Start[:0],
End: t.span.End[:0],
Keys: t.span.Keys[:0],
}
if err := t.Transformer.Transform(t.Compare, *span, &t.span); err != nil {
t.err = err
return nil
}
return &t.span
}
// SeekGE implements the FragmentIterator interface.
func (t *TransformerIter) SeekGE(key []byte) *Span {
span := t.FragmentIterator.SeekGE(key)
if span == nil {
return nil
}
return t.applyTransform(span)
}
// SeekLT implements the FragmentIterator interface.
func (t *TransformerIter) SeekLT(key []byte) *Span {
span := t.FragmentIterator.SeekLT(key)
if span == nil {
return nil
}
return t.applyTransform(span)
}
// First implements the FragmentIterator interface.
func (t *TransformerIter) First() *Span {
span := t.FragmentIterator.First()
if span == nil {
return nil
}
return t.applyTransform(span)
}
// Last implements the FragmentIterator interface.
func (t *TransformerIter) Last() *Span {
span := t.FragmentIterator.Last()
if span == nil {
return nil
}
return t.applyTransform(span)
}
// Next implements the FragmentIterator interface.
func (t *TransformerIter) Next() *Span {
span := t.FragmentIterator.Next()
if span == nil {
return nil
}
return t.applyTransform(span)
}
// Prev implements the FragmentIterator interface.
func (t *TransformerIter) Prev() *Span {
span := t.FragmentIterator.Prev()
if span == nil {
return nil
}
return t.applyTransform(span)
}
// Error implements the FragmentIterator interface.
func (t *TransformerIter) Error() error {
if t.err != nil {
return t.err
}
return t.FragmentIterator.Error()
}
// Close implements the FragmentIterator interface.
func (t *TransformerIter) Close() error {
err := t.FragmentIterator.Close()
if err != nil {
return err
}
return t.err
}

View File

@@ -227,6 +227,12 @@ type FileMetadata struct {
// we'd have to write virtual sstable stats to the version edit. // we'd have to write virtual sstable stats to the version edit.
Stats TableStats Stats TableStats
// Deleted is set to true if a VersionEdit gets installed that has deleted
// this file. Protected by the manifest lock (see versionSet.logLock()).
Deleted bool
// For L0 files only. Protected by DB.mu. Used to generate L0 sublevels and
// pick L0 compactions. Only accurate for the most recent Version.
SubLevel int SubLevel int
L0Index int L0Index int
minIntervalIndex int minIntervalIndex int
@@ -235,9 +241,6 @@ type FileMetadata struct {
// NB: the alignment of this struct is 8 bytes. We pack all the bools to // NB: the alignment of this struct is 8 bytes. We pack all the bools to
// ensure an optimal packing. // ensure an optimal packing.
// For L0 files only. Protected by DB.mu. Used to generate L0 sublevels and
// pick L0 compactions. Only accurate for the most recent Version.
//
// IsIntraL0Compacting is set to True if this file is part of an intra-L0 // IsIntraL0Compacting is set to True if this file is part of an intra-L0
// compaction. When it's true, IsCompacting must also return true. If // compaction. When it's true, IsCompacting must also return true. If
// Compacting is true and IsIntraL0Compacting is false for an L0 file, the // Compacting is true and IsIntraL0Compacting is false for an L0 file, the
@@ -370,6 +373,17 @@ func (m *FileMetadata) InitPhysicalBacking() {
} }
} }
// InitProviderBacking creates a new FileBacking for a file backed by
// an objstorage.Provider.
func (m *FileMetadata) InitProviderBacking(fileNum base.DiskFileNum) {
if !m.Virtual {
panic("pebble: provider-backed sstables must be virtual")
}
if m.FileBacking == nil {
m.FileBacking = &FileBacking{DiskFileNum: fileNum}
}
}
// ValidateVirtual should be called once the FileMetadata for a virtual sstable // ValidateVirtual should be called once the FileMetadata for a virtual sstable
// is created to verify that the fields of the virtual sstable are sound. // is created to verify that the fields of the virtual sstable are sound.
func (m *FileMetadata) ValidateVirtual(createdFrom *FileMetadata) { func (m *FileMetadata) ValidateVirtual(createdFrom *FileMetadata) {

View File

@@ -52,6 +52,8 @@ const (
// Pebble tags. // Pebble tags.
tagNewFile5 = 104 // Range keys. tagNewFile5 = 104 // Range keys.
tagCreatedBackingTable = 105
tagRemovedBackingTable = 106
// The custom tags sub-format used by tagNewFile4 and above. // The custom tags sub-format used by tagNewFile4 and above.
customTagTerminate = 1 customTagTerminate = 1
@@ -59,6 +61,7 @@ const (
customTagCreationTime = 6 customTagCreationTime = 6
customTagPathID = 65 customTagPathID = 65
customTagNonSafeIgnoreMask = 1 << 6 customTagNonSafeIgnoreMask = 1 << 6
customTagVirtual = 66
) )
// DeletedFileEntry holds the state for a file deletion from a level. The file // DeletedFileEntry holds the state for a file deletion from a level. The file
@@ -73,6 +76,9 @@ type DeletedFileEntry struct {
type NewFileEntry struct { type NewFileEntry struct {
Level int Level int
Meta *FileMetadata Meta *FileMetadata
// BackingFileNum is only set during manifest replay, and only for virtual
// sstables.
BackingFileNum base.DiskFileNum
} }
// VersionEdit holds the state for an edit to a Version along with other // VersionEdit holds the state for an edit to a Version along with other
@@ -143,7 +149,9 @@ type VersionEdit struct {
// Decode decodes an edit from the specified reader. // Decode decodes an edit from the specified reader.
// //
// TODO(bananabrick): Support decoding of virtual sstable state. // Note that the Decode step will not set the FileBacking for virtual sstables
// and the responsibility is left to the caller. However, the Decode step will
// populate the NewFileEntry.BackingFileNum in VersionEdit.NewFiles.
func (v *VersionEdit) Decode(r io.Reader) error { func (v *VersionEdit) Decode(r io.Reader) error {
br, ok := r.(byteReader) br, ok := r.(byteReader)
if !ok { if !ok {
@@ -196,6 +204,28 @@ func (v *VersionEdit) Decode(r io.Reader) error {
} }
// NB: RocksDB does not use compaction pointers anymore. // NB: RocksDB does not use compaction pointers anymore.
case tagRemovedBackingTable:
n, err := d.readUvarint()
if err != nil {
return err
}
v.RemovedBackingTables = append(
v.RemovedBackingTables, base.FileNum(n).DiskFileNum(),
)
case tagCreatedBackingTable:
dfn, err := d.readUvarint()
if err != nil {
return err
}
size, err := d.readUvarint()
if err != nil {
return err
}
fileBacking := &FileBacking{
DiskFileNum: base.FileNum(dfn).DiskFileNum(),
Size: size,
}
v.CreatedBackingTables = append(v.CreatedBackingTables, fileBacking)
case tagDeletedFile: case tagDeletedFile:
level, err := d.readLevel() level, err := d.readLevel()
if err != nil { if err != nil {
@@ -301,6 +331,10 @@ func (v *VersionEdit) Decode(r io.Reader) error {
} }
var markedForCompaction bool var markedForCompaction bool
var creationTime uint64 var creationTime uint64
virtualState := struct {
virtual bool
backingFileNum uint64
}{}
if tag == tagNewFile4 || tag == tagNewFile5 { if tag == tagNewFile4 || tag == tagNewFile5 {
for { for {
customTag, err := d.readUvarint() customTag, err := d.readUvarint()
@@ -309,7 +343,16 @@ func (v *VersionEdit) Decode(r io.Reader) error {
} }
if customTag == customTagTerminate { if customTag == customTagTerminate {
break break
} else if customTag == customTagVirtual {
virtualState.virtual = true
n, err := d.readUvarint()
if err != nil {
return err
} }
virtualState.backingFileNum = n
continue
}
field, err := d.readBytes() field, err := d.readBytes()
if err != nil { if err != nil {
return err return err
@@ -345,6 +388,7 @@ func (v *VersionEdit) Decode(r io.Reader) error {
SmallestSeqNum: smallestSeqNum, SmallestSeqNum: smallestSeqNum,
LargestSeqNum: largestSeqNum, LargestSeqNum: largestSeqNum,
MarkedForCompaction: markedForCompaction, MarkedForCompaction: markedForCompaction,
Virtual: virtualState.virtual,
} }
if tag != tagNewFile5 { // no range keys present if tag != tagNewFile5 { // no range keys present
m.SmallestPointKey = base.DecodeInternalKey(smallestPointKey) m.SmallestPointKey = base.DecodeInternalKey(smallestPointKey)
@@ -376,11 +420,18 @@ func (v *VersionEdit) Decode(r io.Reader) error {
} }
} }
m.boundsSet = true m.boundsSet = true
if !virtualState.virtual {
m.InitPhysicalBacking() m.InitPhysicalBacking()
v.NewFiles = append(v.NewFiles, NewFileEntry{ }
nfe := NewFileEntry{
Level: level, Level: level,
Meta: m, Meta: m,
}) }
if virtualState.virtual {
nfe.BackingFileNum = base.FileNum(virtualState.backingFileNum).DiskFileNum()
}
v.NewFiles = append(v.NewFiles, nfe)
case tagPrevLogNumber: case tagPrevLogNumber:
n, err := d.readUvarint() n, err := d.readUvarint()
@@ -442,8 +493,6 @@ func (v *VersionEdit) String() string {
} }
// Encode encodes an edit to the specified writer. // Encode encodes an edit to the specified writer.
//
// TODO(bananabrick): Support encoding of virtual sstable state.
func (v *VersionEdit) Encode(w io.Writer) error { func (v *VersionEdit) Encode(w io.Writer) error {
e := versionEditEncoder{new(bytes.Buffer)} e := versionEditEncoder{new(bytes.Buffer)}
@@ -463,6 +512,15 @@ func (v *VersionEdit) Encode(w io.Writer) error {
e.writeUvarint(tagNextFileNumber) e.writeUvarint(tagNextFileNumber)
e.writeUvarint(uint64(v.NextFileNum)) e.writeUvarint(uint64(v.NextFileNum))
} }
for _, dfn := range v.RemovedBackingTables {
e.writeUvarint(tagRemovedBackingTable)
e.writeUvarint(uint64(dfn.FileNum()))
}
for _, fileBacking := range v.CreatedBackingTables {
e.writeUvarint(tagCreatedBackingTable)
e.writeUvarint(uint64(fileBacking.DiskFileNum.FileNum()))
e.writeUvarint(fileBacking.Size)
}
// RocksDB requires LastSeqNum to be encoded for the first MANIFEST entry, // RocksDB requires LastSeqNum to be encoded for the first MANIFEST entry,
// even though its value is zero. We detect this by encoding LastSeqNum when // even though its value is zero. We detect this by encoding LastSeqNum when
// ComparerName is set. // ComparerName is set.
@@ -476,7 +534,7 @@ func (v *VersionEdit) Encode(w io.Writer) error {
e.writeUvarint(uint64(x.FileNum)) e.writeUvarint(uint64(x.FileNum))
} }
for _, x := range v.NewFiles { for _, x := range v.NewFiles {
customFields := x.Meta.MarkedForCompaction || x.Meta.CreationTime != 0 customFields := x.Meta.MarkedForCompaction || x.Meta.CreationTime != 0 || x.Meta.Virtual
var tag uint64 var tag uint64
switch { switch {
case x.Meta.HasRangeKeys: case x.Meta.HasRangeKeys:
@@ -529,6 +587,10 @@ func (v *VersionEdit) Encode(w io.Writer) error {
e.writeUvarint(customTagNeedsCompaction) e.writeUvarint(customTagNeedsCompaction)
e.writeBytes([]byte{1}) e.writeBytes([]byte{1})
} }
if x.Meta.Virtual {
e.writeUvarint(customTagVirtual)
e.writeUvarint(uint64(x.Meta.FileBacking.DiskFileNum.FileNum()))
}
e.writeUvarint(customTagTerminate) e.writeUvarint(customTagTerminate)
} }
} }
@@ -536,6 +598,7 @@ func (v *VersionEdit) Encode(w io.Writer) error {
return err return err
} }
// versionEditDecoder should be used to decode version edits.
type versionEditDecoder struct { type versionEditDecoder struct {
byteReader byteReader
} }
@@ -634,7 +697,9 @@ type BulkVersionEdit struct {
Added [NumLevels]map[base.FileNum]*FileMetadata Added [NumLevels]map[base.FileNum]*FileMetadata
Deleted [NumLevels]map[base.FileNum]*FileMetadata Deleted [NumLevels]map[base.FileNum]*FileMetadata
AddedFileBacking []*FileBacking // AddedFileBacking is a map to support lookup so that we can populate the
// FileBacking of virtual sstables during manifest replay.
AddedFileBacking map[base.DiskFileNum]*FileBacking
RemovedFileBacking []base.DiskFileNum RemovedFileBacking []base.DiskFileNum
// AddedByFileNum maps file number to file metadata for all added files // AddedByFileNum maps file number to file metadata for all added files
@@ -697,6 +762,16 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
} }
} }
// Generate state for Added backing files. Note that these must be generated
// before we loop through the NewFiles, because we need to populate the
// FileBackings which might be used by the NewFiles loop.
if b.AddedFileBacking == nil {
b.AddedFileBacking = make(map[base.DiskFileNum]*FileBacking)
}
for _, fb := range ve.CreatedBackingTables {
b.AddedFileBacking[fb.DiskFileNum] = fb
}
for _, nf := range ve.NewFiles { for _, nf := range ve.NewFiles {
// A new file should not have been deleted in this or a preceding // A new file should not have been deleted in this or a preceding
// VersionEdit at the same level (though files can move across levels). // VersionEdit at the same level (though files can move across levels).
@@ -705,6 +780,17 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
return base.CorruptionErrorf("pebble: file deleted L%d.%s before it was inserted", nf.Level, nf.Meta.FileNum) return base.CorruptionErrorf("pebble: file deleted L%d.%s before it was inserted", nf.Level, nf.Meta.FileNum)
} }
} }
if nf.Meta.Virtual && nf.Meta.FileBacking == nil {
// FileBacking for a virtual sstable must only be nil if we're performing
// manifest replay.
nf.Meta.FileBacking = b.AddedFileBacking[nf.BackingFileNum]
if nf.Meta.FileBacking == nil {
return errors.Errorf("FileBacking for virtual sstable must not be nil")
}
} else if nf.Meta.FileBacking == nil {
return errors.Errorf("Added file L%d.%s's has no FileBacking", nf.Level, nf.Meta.FileNum)
}
if b.Added[nf.Level] == nil { if b.Added[nf.Level] == nil {
b.Added[nf.Level] = make(map[base.FileNum]*FileMetadata) b.Added[nf.Level] = make(map[base.FileNum]*FileMetadata)
} }
@@ -717,9 +803,6 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error {
} }
} }
// Generate state for the backing files.
b.AddedFileBacking = append(b.AddedFileBacking, ve.CreatedBackingTables...)
// Since a file can be removed from backing files in exactly one version // Since a file can be removed from backing files in exactly one version
// edit it is safe to just append without any de-duplication. // edit it is safe to just append without any de-duplication.
b.RemovedFileBacking = append(b.RemovedFileBacking, ve.RemovedBackingTables...) b.RemovedFileBacking = append(b.RemovedFileBacking, ve.RemovedBackingTables...)

View File

@@ -6,6 +6,7 @@ package rangekey
import ( import (
"bytes" "bytes"
"fmt"
"math" "math"
"sort" "sort"
@@ -367,3 +368,78 @@ func coalesce(
} }
return nil return nil
} }
// ForeignSSTTransformer implements a keyspan.Transformer for range keys in
// foreign sstables (i.e. shared sstables not created by us). It is largely
// similar to the Transform function implemented in UserIteratorConfig in that
// it calls coalesce to remove range keys shadowed by other range keys, but also
// retains the range key that does the shadowing. In addition, it elides
// RangeKey unsets/dels in L6 as they are inapplicable when reading from a
// different Pebble instance.
type ForeignSSTTransformer struct {
Comparer *base.Comparer
Level int
sortBuf keyspan.KeysBySuffix
}
// Transform implements the Transformer interface.
func (f *ForeignSSTTransformer) Transform(
cmp base.Compare, s keyspan.Span, dst *keyspan.Span,
) error {
// Apply shadowing of keys.
dst.Start = s.Start
dst.End = s.End
f.sortBuf = keyspan.KeysBySuffix{
Cmp: cmp,
Keys: f.sortBuf.Keys[:0],
}
if err := coalesce(f.Comparer.Equal, &f.sortBuf, math.MaxUint64, s.Keys); err != nil {
return err
}
keys := f.sortBuf.Keys
dst.Keys = dst.Keys[:0]
for i := range keys {
seqNum := keys[i].SeqNum()
switch keys[i].Kind() {
case base.InternalKeyKindRangeKeySet:
if invariants.Enabled && len(dst.Keys) > 0 && cmp(dst.Keys[len(dst.Keys)-1].Suffix, keys[i].Suffix) > 0 {
panic("pebble: keys unexpectedly not in ascending suffix order")
}
switch f.Level {
case 5:
fallthrough
case 6:
if seqNum != base.SeqNumForLevel(f.Level) {
panic(fmt.Sprintf("pebble: expected range key iter to return seqnum %d, got %d", base.SeqNumForLevel(f.Level), seqNum))
}
}
case base.InternalKeyKindRangeKeyUnset:
if invariants.Enabled && len(dst.Keys) > 0 && cmp(dst.Keys[len(dst.Keys)-1].Suffix, keys[i].Suffix) > 0 {
panic("pebble: keys unexpectedly not in ascending suffix order")
}
fallthrough
case base.InternalKeyKindRangeKeyDelete:
switch f.Level {
case 5:
// Emit this key.
if seqNum != base.SeqNumForLevel(f.Level) {
panic(fmt.Sprintf("pebble: expected range key iter to return seqnum %d, got %d", base.SeqNumForLevel(f.Level), seqNum))
}
case 6:
// Skip this key, as foreign sstable in L6 do not need to emit range key
// unsets/dels as they do not apply to any other sstables.
continue
}
default:
return base.CorruptionErrorf("pebble: unrecognized range key kind %s", keys[i].Kind())
}
dst.Keys = append(dst.Keys, keyspan.Key{
Trailer: base.MakeTrailer(seqNum, keys[i].Kind()),
Suffix: keys[i].Suffix,
Value: keys[i].Value,
})
}
// coalesce results in dst.Keys being sorted by Suffix.
dst.KeysOrder = keyspan.BySuffixAsc
return nil
}

View File

@@ -568,7 +568,10 @@ func (i *Iterator) findNextEntry(limit []byte) {
i.iterValidityState = IterValid i.iterValidityState = IterValid
return return
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
// NB: treating InternalKeyKindSingleDelete as equivalent to DEL is not
// only simpler, but is also necessary for correctness due to
// InternalKeyKindSSTableInternalObsoleteBit.
i.nextUserKey() i.nextUserKey()
continue continue
@@ -631,7 +634,10 @@ func (i *Iterator) nextPointCurrentUserKey() bool {
i.err = base.CorruptionErrorf("pebble: unexpected range key set mid-user key") i.err = base.CorruptionErrorf("pebble: unexpected range key set mid-user key")
return false return false
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
// NB: treating InternalKeyKindSingleDelete as equivalent to DEL is not
// only simpler, but is also necessary for correctness due to
// InternalKeyKindSSTableInternalObsoleteBit.
return false return false
case InternalKeyKindSet, InternalKeyKindSetWithDelete: case InternalKeyKindSet, InternalKeyKindSetWithDelete:
@@ -929,7 +935,7 @@ func (i *Iterator) findPrevEntry(limit []byte) {
// return the key even if the MERGE point key is deleted. // return the key even if the MERGE point key is deleted.
rangeKeyBoundary = true rangeKeyBoundary = true
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
i.value = LazyValue{} i.value = LazyValue{}
i.iterValidityState = IterExhausted i.iterValidityState = IterExhausted
valueMerger = nil valueMerger = nil
@@ -1092,9 +1098,13 @@ func (i *Iterator) mergeNext(key InternalKey, valueMerger ValueMerger) {
return return
} }
switch key.Kind() { switch key.Kind() {
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized:
// We've hit a deletion tombstone. Return everything up to this // We've hit a deletion tombstone. Return everything up to this
// point. // point.
//
// NB: treating InternalKeyKindSingleDelete as equivalent to DEL is not
// only simpler, but is also necessary for correctness due to
// InternalKeyKindSSTableInternalObsoleteBit.
return return
case InternalKeyKindSet, InternalKeyKindSetWithDelete: case InternalKeyKindSet, InternalKeyKindSetWithDelete:

View File

@@ -165,7 +165,7 @@ func (m *simpleMergingIter) step() bool {
if m.valueMerger != nil { if m.valueMerger != nil {
// Ongoing series of MERGE records. // Ongoing series of MERGE records.
switch item.key.Kind() { switch item.key.Kind() {
case InternalKeyKindSingleDelete, InternalKeyKindDelete: case InternalKeyKindSingleDelete, InternalKeyKindDelete, InternalKeyKindDeleteSized:
var closer io.Closer var closer io.Closer
_, closer, m.err = m.valueMerger.Finish(true /* includesBase */) _, closer, m.err = m.valueMerger.Finish(true /* includesBase */)
if m.err == nil && closer != nil { if m.err == nil && closer != nil {
@@ -379,7 +379,7 @@ func checkRangeTombstones(c *checkConfig) error {
atomicUnit, _ := expandToAtomicUnit(c.cmp, lf.Slice(), true /* disableIsCompacting */) atomicUnit, _ := expandToAtomicUnit(c.cmp, lf.Slice(), true /* disableIsCompacting */)
lower, upper := manifest.KeyRange(c.cmp, atomicUnit.Iter()) lower, upper := manifest.KeyRange(c.cmp, atomicUnit.Iter())
iterToClose, iter, err := c.newIters( iterToClose, iter, err := c.newIters(
context.Background(), lf.FileMetadata, nil, internalIterOpts{}) context.Background(), lf.FileMetadata, &IterOptions{level: manifest.Level(lsmLevel)}, internalIterOpts{})
if err != nil { if err != nil {
return err return err
} }

View File

@@ -36,9 +36,9 @@ type tableNewIters func(
// tableNewRangeDelIter takes a tableNewIters and returns a TableNewSpanIter // tableNewRangeDelIter takes a tableNewIters and returns a TableNewSpanIter
// for the rangedel iterator returned by tableNewIters. // for the rangedel iterator returned by tableNewIters.
func tableNewRangeDelIter(ctx context.Context, newIters tableNewIters) keyspan.TableNewSpanIter { func tableNewRangeDelIter(ctx context.Context, newIters tableNewIters) keyspan.TableNewSpanIter {
return func(file *manifest.FileMetadata, iterOptions *keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) { return func(file *manifest.FileMetadata, iterOptions keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) {
iter, rangeDelIter, err := newIters( iter, rangeDelIter, err := newIters(
ctx, file, &IterOptions{RangeKeyFilters: iterOptions.RangeKeyFilters}, internalIterOpts{}) ctx, file, &IterOptions{RangeKeyFilters: iterOptions.RangeKeyFilters, level: iterOptions.Level}, internalIterOpts{})
if iter != nil { if iter != nil {
_ = iter.Close() _ = iter.Close()
} }
@@ -200,6 +200,11 @@ type levelIter struct {
// cache when constructing new table iterators. // cache when constructing new table iterators.
internalOpts internalIterOpts internalOpts internalIterOpts
// Scratch space for the obsolete keys filter, when there are no other block
// property filters specified. See the performance note where
// IterOptions.PointKeyFilters is declared.
filtersBuf [1]BlockPropertyFilter
// Disable invariant checks even if they are otherwise enabled. Used by tests // Disable invariant checks even if they are otherwise enabled. Used by tests
// which construct "impossible" situations (e.g. seeking to a key before the // which construct "impossible" situations (e.g. seeking to a key before the
// lower bound). // lower bound).
@@ -267,8 +272,12 @@ func (l *levelIter) init(
l.upper = opts.UpperBound l.upper = opts.UpperBound
l.tableOpts.TableFilter = opts.TableFilter l.tableOpts.TableFilter = opts.TableFilter
l.tableOpts.PointKeyFilters = opts.PointKeyFilters l.tableOpts.PointKeyFilters = opts.PointKeyFilters
if len(opts.PointKeyFilters) == 0 {
l.tableOpts.PointKeyFilters = l.filtersBuf[:0:1]
}
l.tableOpts.UseL6Filters = opts.UseL6Filters l.tableOpts.UseL6Filters = opts.UseL6Filters
l.tableOpts.level = l.level l.tableOpts.level = l.level
l.tableOpts.snapshotForHideObsoletePoints = opts.snapshotForHideObsoletePoints
l.cmp = cmp l.cmp = cmp
l.split = split l.split = split
l.iterFile = nil l.iterFile = nil

View File

@@ -184,6 +184,11 @@ type Metrics struct {
Duration time.Duration Duration time.Duration
} }
Ingest struct {
// The total number of ingestions
Count uint64
}
Flush struct { Flush struct {
// The total number of flushes. // The total number of flushes.
Count int64 Count int64
@@ -225,6 +230,9 @@ type Metrics struct {
// The approximate count of internal tombstones (DEL, SINGLEDEL and // The approximate count of internal tombstones (DEL, SINGLEDEL and
// RANGEDEL key kinds) within the database. // RANGEDEL key kinds) within the database.
TombstoneCount uint64 TombstoneCount uint64
// A cumulative total number of missized DELSIZED keys encountered by
// compactions since the database was opened.
MissizedTombstonesCount uint64
} }
Snapshots struct { Snapshots struct {
@@ -504,6 +512,9 @@ func (m *Metrics) SafeFormat(w redact.SafePrinter, _ rune) {
notApplicable, notApplicable,
notApplicable, notApplicable,
redact.Safe(hitRate(m.Filter.Hits, m.Filter.Misses))) redact.Safe(hitRate(m.Filter.Hits, m.Filter.Misses)))
w.Printf(" ingest %9d\n",
redact.Safe(m.Ingest.Count),
)
} }
func hitRate(hits, misses int64) float64 { func hitRate(hits, misses int64) float64 {

View File

@@ -27,8 +27,8 @@ func (h *NoopReadHandle) ReadAt(ctx context.Context, p []byte, off int64) error
// Close is part of the ReadHandle interface. // Close is part of the ReadHandle interface.
func (*NoopReadHandle) Close() error { return nil } func (*NoopReadHandle) Close() error { return nil }
// MaxReadahead is part of the ReadHandle interface. // SetupForCompaction is part of the ReadHandle interface.
func (*NoopReadHandle) MaxReadahead() {} func (*NoopReadHandle) SetupForCompaction() {}
// RecordCacheHit is part of the ReadHandle interface. // RecordCacheHit is part of the ReadHandle interface.
func (*NoopReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {} func (*NoopReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {}

View File

@@ -16,6 +16,9 @@ import (
type Readable interface { type Readable interface {
// ReadAt reads len(p) bytes into p starting at offset off. // ReadAt reads len(p) bytes into p starting at offset off.
// //
// Does not return partial results; if off + len(p) is past the end of the
// object, an error is returned.
//
// Clients of ReadAt can execute parallel ReadAt calls on the // Clients of ReadAt can execute parallel ReadAt calls on the
// same Readable. // same Readable.
ReadAt(ctx context.Context, p []byte, off int64) error ReadAt(ctx context.Context, p []byte, off int64) error
@@ -39,14 +42,18 @@ type Readable interface {
type ReadHandle interface { type ReadHandle interface {
// ReadAt reads len(p) bytes into p starting at offset off. // ReadAt reads len(p) bytes into p starting at offset off.
// //
// Does not return partial results; if off + len(p) is past the end of the
// object, an error is returned.
//
// Parallel ReadAt calls on the same ReadHandle are not allowed. // Parallel ReadAt calls on the same ReadHandle are not allowed.
ReadAt(ctx context.Context, p []byte, off int64) error ReadAt(ctx context.Context, p []byte, off int64) error
Close() error Close() error
// MaxReadahead configures the implementation to expect large sequential // SetupForCompaction informs the implementation that the read handle will
// reads. Used to skip any initial read-ahead ramp-up. // be used to read data blocks for a compaction. The implementation can expect
MaxReadahead() // sequential reads, and can decide to not retain data in any caches.
SetupForCompaction()
// RecordCacheHit informs the implementation that we were able to retrieve a // RecordCacheHit informs the implementation that we were able to retrieve a
// block from cache. This is useful for example when the implementation is // block from cache. This is useful for example when the implementation is
@@ -209,6 +216,11 @@ type Provider interface {
// Cannot be called if shared storage is not configured for the provider. // Cannot be called if shared storage is not configured for the provider.
SetCreatorID(creatorID CreatorID) error SetCreatorID(creatorID CreatorID) error
// IsForeign returns whether this object is owned by a different node. Return
// value undefined if creator ID is not set yet, or if this object does not
// exist in this provider.
IsForeign(meta ObjectMetadata) bool
// SharedObjectBacking encodes the shared object metadata. // SharedObjectBacking encodes the shared object metadata.
SharedObjectBacking(meta *ObjectMetadata) (SharedObjectBackingHandle, error) SharedObjectBacking(meta *ObjectMetadata) (SharedObjectBackingHandle, error)

View File

@@ -16,9 +16,9 @@ const (
// RecordCacheHitOp happens when a read is satisfied from the block cache. See // RecordCacheHitOp happens when a read is satisfied from the block cache. See
// objstorage.ReadHandle.RecordCacheHit(). // objstorage.ReadHandle.RecordCacheHit().
RecordCacheHitOp RecordCacheHitOp
// MaxReadaheadOp is a "meta operation" that configures a read handle for // SetupForCompactionOp is a "meta operation" that configures a read handle
// large sequential reads. See objstorage.ReadHandle.MaxReadahead(). // for large sequential reads. See objstorage.ReadHandle.SetupForCompaction().
MaxReadaheadOp SetupForCompactionOp
) )
// Reason indicates the higher-level context of the operation. // Reason indicates the higher-level context of the operation.

View File

@@ -209,14 +209,14 @@ func (rh *readHandle) Close() error {
return rh.rh.Close() return rh.rh.Close()
} }
// MaxReadahead is part of the objstorage.ReadHandle interface. // SetupForCompaction is part of the objstorage.ReadHandle interface.
func (rh *readHandle) MaxReadahead() { func (rh *readHandle) SetupForCompaction() {
rh.g.add(context.Background(), Event{ rh.g.add(context.Background(), Event{
Op: MaxReadaheadOp, Op: SetupForCompactionOp,
FileNum: rh.fileNum, FileNum: rh.fileNum,
HandleID: rh.handleID, HandleID: rh.handleID,
}) })
rh.rh.MaxReadahead() rh.rh.SetupForCompaction()
} }
// RecordCacheHit is part of the objstorage.ReadHandle interface. // RecordCacheHit is part of the objstorage.ReadHandle interface.

View File

@@ -93,6 +93,21 @@ type Settings struct {
// (experimental). // (experimental).
Shared struct { Shared struct {
Storage shared.Storage Storage shared.Storage
// CacheSizeBytes is the size of the on-disk block cache for objects
// on shared storage. If it is 0, no cache is used.
CacheSizeBytes int64
// CacheBlockSize is the block size of the cache; if 0, the default of 32KB is used.
CacheBlockSize int
// The number of independent shards the cache leverages. Each shard is the same size,
// and a hash of filenum & offset map a read to a certain shard. If set to 0,
// 2*runtime.GOMAXPROCS is used as the shard count.
CacheShardCount int
// TODO(radu): allow the cache to live on another FS/location (e.g. to use
// instance-local SSD).
} }
} }

View File

@@ -12,7 +12,6 @@ const (
// TODO(bilal): Have the initial size value be a factor of the block size, // TODO(bilal): Have the initial size value be a factor of the block size,
// as opposed to a hardcoded value. // as opposed to a hardcoded value.
initialReadaheadSize = 64 << 10 /* 64KB */ initialReadaheadSize = 64 << 10 /* 64KB */
maxReadaheadSize = 256 << 10 /* 256KB */
) )
// readaheadState contains state variables related to readahead. Updated on // readaheadState contains state variables related to readahead. Updated on
@@ -20,6 +19,7 @@ const (
type readaheadState struct { type readaheadState struct {
// Number of sequential reads. // Number of sequential reads.
numReads int64 numReads int64
maxReadaheadSize int64
// Size issued to the next call to Prefetch. Starts at or above // Size issued to the next call to Prefetch. Starts at or above
// initialReadaheadSize and grows exponentially until maxReadaheadSize. // initialReadaheadSize and grows exponentially until maxReadaheadSize.
size int64 size int64
@@ -32,22 +32,23 @@ type readaheadState struct {
limit int64 limit int64
} }
func makeReadaheadState() readaheadState { func makeReadaheadState(maxReadaheadSize int64) readaheadState {
return readaheadState{ return readaheadState{
size: initialReadaheadSize, size: initialReadaheadSize,
maxReadaheadSize: maxReadaheadSize,
} }
} }
func (rs *readaheadState) recordCacheHit(offset, blockLength int64) { func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
currentReadEnd := offset + blockLength currentReadEnd := offset + blockLength
if rs.numReads >= minFileReadsForReadahead { if rs.numReads >= minFileReadsForReadahead {
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
// This is a read that would have resulted in a readahead, had it // This is a read that would have resulted in a readahead, had it
// not been a cache hit. // not been a cache hit.
rs.limit = currentReadEnd rs.limit = currentReadEnd
return return
} }
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+maxReadaheadSize { if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+rs.maxReadaheadSize {
// We read too far away from rs.limit to benefit from readahead in // We read too far away from rs.limit to benefit from readahead in
// any scenario. Reset all variables. // any scenario. Reset all variables.
rs.numReads = 1 rs.numReads = 1
@@ -61,7 +62,7 @@ func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
// readahead. // readahead.
return return
} }
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
// Blocks are being read sequentially and would benefit from readahead // Blocks are being read sequentially and would benefit from readahead
// down the line. // down the line.
rs.numReads++ rs.numReads++
@@ -89,28 +90,28 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
// as well as the interval where a read would benefit from read ahead: // as well as the interval where a read would benefit from read ahead:
// [rs.limit, rs.limit + rs.size] // [rs.limit, rs.limit + rs.size]
// We increase the latter interval to // We increase the latter interval to
// [rs.limit, rs.limit + maxReadaheadSize] to account for cases where // [rs.limit, rs.limit + rs.maxReadaheadSize] to account for cases where
// readahead may not be beneficial with a small readahead size, but over // readahead may not be beneficial with a small readahead size, but over
// time the readahead size would increase exponentially to make it // time the readahead size would increase exponentially to make it
// beneficial. // beneficial.
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
// We are doing a read in the interval ahead of // We are doing a read in the interval ahead of
// the last readahead range. In the diagrams below, ++++ is the last // the last readahead range. In the diagrams below, ++++ is the last
// readahead range, ==== is the range represented by // readahead range, ==== is the range represented by
// [rs.limit, rs.limit + maxReadaheadSize], and ---- is the range // [rs.limit, rs.limit + rs.maxReadaheadSize], and ---- is the range
// being read. // being read.
// //
// rs.limit rs.limit + maxReadaheadSize // rs.limit rs.limit + rs.maxReadaheadSize
// ++++++++++|===========================| // ++++++++++|===========================|
// //
// |-------------| // |-------------|
// offset currentReadEnd // offset currentReadEnd
// //
// This case is also possible, as are all cases with an overlap // This case is also possible, as are all cases with an overlap
// between [rs.limit, rs.limit + maxReadaheadSize] and [offset, // between [rs.limit, rs.limit + rs.maxReadaheadSize] and [offset,
// currentReadEnd]: // currentReadEnd]:
// //
// rs.limit rs.limit + maxReadaheadSize // rs.limit rs.limit + rs.maxReadaheadSize
// ++++++++++|===========================| // ++++++++++|===========================|
// //
// |-------------| // |-------------|
@@ -122,19 +123,19 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
rs.prevSize = rs.size rs.prevSize = rs.size
// Increase rs.size for the next read. // Increase rs.size for the next read.
rs.size *= 2 rs.size *= 2
if rs.size > maxReadaheadSize { if rs.size > rs.maxReadaheadSize {
rs.size = maxReadaheadSize rs.size = rs.maxReadaheadSize
} }
return rs.prevSize return rs.prevSize
} }
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+maxReadaheadSize { if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+rs.maxReadaheadSize {
// The above conditional has rs.limit > rs.prevSize to confirm that // The above conditional has rs.limit > rs.prevSize to confirm that
// rs.limit - rs.prevSize would not underflow. // rs.limit - rs.prevSize would not underflow.
// We read too far away from rs.limit to benefit from readahead in // We read too far away from rs.limit to benefit from readahead in
// any scenario. Reset all variables. // any scenario. Reset all variables.
// The case where we read too far ahead: // The case where we read too far ahead:
// //
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize) // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize)
// |+++++++++++++|=============| // |+++++++++++++|=============|
// //
// |-------------| // |-------------|
@@ -142,7 +143,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
// //
// Or too far behind: // Or too far behind:
// //
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize) // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize)
// |+++++++++++++|=============| // |+++++++++++++|=============|
// //
// |-------------| // |-------------|
@@ -160,7 +161,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
// readahead, but there's no reason to issue a readahead call at the // readahead, but there's no reason to issue a readahead call at the
// moment. // moment.
// //
// (rs.limit - rs.prevSize) (rs.limit + maxReadaheadSize) // (rs.limit - rs.prevSize) (rs.limit + rs.maxReadaheadSize)
// |+++++++++++++|===============| // |+++++++++++++|===============|
// (rs.limit) // (rs.limit)
// //
@@ -170,11 +171,11 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
rs.numReads++ rs.numReads++
return 0 return 0
} }
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
// Blocks are being read sequentially and would benefit from readahead // Blocks are being read sequentially and would benefit from readahead
// down the line. // down the line.
// //
// (rs.limit) (rs.limit + maxReadaheadSize) // (rs.limit) (rs.limit + rs.maxReadaheadSize)
// |=============| // |=============|
// //
// |-------| // |-------|
@@ -186,7 +187,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
// We read too far ahead of the last read, or before it. This indicates // We read too far ahead of the last read, or before it. This indicates
// a random read, where readahead is not desirable. Reset all variables. // a random read, where readahead is not desirable. Reset all variables.
// //
// (rs.limit - maxReadaheadSize) (rs.limit) (rs.limit + maxReadaheadSize) // (rs.limit - rs.maxReadaheadSize) (rs.limit) (rs.limit + rs.maxReadaheadSize)
// |+++++++++++++|=============| // |+++++++++++++|=============|
// //
// |-------| // |-------|

View File

@@ -6,6 +6,7 @@ package objstorageprovider
import ( import (
"context" "context"
"runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -13,6 +14,7 @@ import (
"github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/objstorage"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedobjcat" "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedobjcat"
"github.com/cockroachdb/pebble/objstorage/shared" "github.com/cockroachdb/pebble/objstorage/shared"
) )
@@ -21,6 +23,7 @@ import (
// All fields remain unset if shared storage is not configured. // All fields remain unset if shared storage is not configured.
type sharedSubsystem struct { type sharedSubsystem struct {
catalog *sharedobjcat.Catalog catalog *sharedobjcat.Catalog
cache *sharedcache.Cache
// checkRefsOnOpen controls whether we check the ref marker file when opening // checkRefsOnOpen controls whether we check the ref marker file when opening
// an object. Normally this is true when invariants are enabled (but the provider // an object. Normally this is true when invariants are enabled (but the provider
@@ -65,6 +68,24 @@ func (p *provider) sharedInit() error {
p.st.Logger.Infof("shared storage configured; no creatorID yet") p.st.Logger.Infof("shared storage configured; no creatorID yet")
} }
if p.st.Shared.CacheSizeBytes > 0 {
const defaultBlockSize = 32 * 1024
blockSize := p.st.Shared.CacheBlockSize
if blockSize == 0 {
blockSize = defaultBlockSize
}
numShards := p.st.Shared.CacheShardCount
if numShards == 0 {
numShards = 2 * runtime.GOMAXPROCS(0)
}
p.shared.cache, err = sharedcache.Open(p.st.FS, p.st.Logger, p.st.FSDirName, blockSize, p.st.Shared.CacheSizeBytes, numShards)
if err != nil {
return errors.Wrapf(err, "pebble: could not open shared object cache")
}
}
for _, meta := range contents.Objects { for _, meta := range contents.Objects {
o := objstorage.ObjectMetadata{ o := objstorage.ObjectMetadata{
DiskFileNum: meta.FileNum, DiskFileNum: meta.FileNum,
@@ -95,6 +116,14 @@ func (p *provider) SetCreatorID(creatorID objstorage.CreatorID) error {
return nil return nil
} }
// IsForeign is part of the objstorage.Provider interface.
func (p *provider) IsForeign(meta objstorage.ObjectMetadata) bool {
if !p.shared.initialized.Load() {
return false
}
return meta.IsShared() && p.shared.creatorID != meta.Shared.CreatorID
}
func (p *provider) sharedCheckInitialized() error { func (p *provider) sharedCheckInitialized() error {
if p.sharedStorage() == nil { if p.sharedStorage() == nil {
return errors.Errorf("shared object support not configured") return errors.Errorf("shared object support not configured")
@@ -211,7 +240,7 @@ func (p *provider) sharedOpenForReading(
} }
return nil, err return nil, err
} }
return newSharedReadable(reader, size), nil return p.newSharedReadable(reader, size, meta.DiskFileNum), nil
} }
func (p *provider) sharedSize(meta objstorage.ObjectMetadata) (int64, error) { func (p *provider) sharedSize(meta objstorage.ObjectMetadata) (int64, error) {

View File

@@ -6,28 +6,55 @@ package objstorageprovider
import ( import (
"context" "context"
"io"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/objstorage"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache"
"github.com/cockroachdb/pebble/objstorage/shared" "github.com/cockroachdb/pebble/objstorage/shared"
) )
const sharedMaxReadaheadSize = 1024 * 1024 /* 1MB */
// sharedReadable is a very simple implementation of Readable on top of the // sharedReadable is a very simple implementation of Readable on top of the
// ReadCloser returned by shared.Storage.CreateObject. // ReadCloser returned by shared.Storage.CreateObject.
type sharedReadable struct { type sharedReadable struct {
objReader shared.ObjectReader objReader shared.ObjectReader
size int64 size int64
fileNum base.DiskFileNum
provider *provider
} }
var _ objstorage.Readable = (*sharedReadable)(nil) var _ objstorage.Readable = (*sharedReadable)(nil)
func newSharedReadable(objReader shared.ObjectReader, size int64) *sharedReadable { func (p *provider) newSharedReadable(
objReader shared.ObjectReader, size int64, fileNum base.DiskFileNum,
) *sharedReadable {
return &sharedReadable{ return &sharedReadable{
objReader: objReader, objReader: objReader,
size: size, size: size,
fileNum: fileNum,
provider: p,
} }
} }
// ReadAt is part of the objstorage.Readable interface.
func (r *sharedReadable) ReadAt(ctx context.Context, p []byte, offset int64) error { func (r *sharedReadable) ReadAt(ctx context.Context, p []byte, offset int64) error {
return r.readInternal(ctx, p, offset, false /* forCompaction */)
}
// readInternal performs a read for the object, using the cache when
// appropriate.
func (r *sharedReadable) readInternal(
ctx context.Context, p []byte, offset int64, forCompaction bool,
) error {
if cache := r.provider.shared.cache; cache != nil {
flags := sharedcache.ReadFlags{
// Don't add data to the cache if this read is for a compaction.
ReadOnly: forCompaction,
}
return r.provider.shared.cache.ReadAt(ctx, r.fileNum, p, offset, r.objReader, r.size, flags)
}
return r.objReader.ReadAt(ctx, p, offset) return r.objReader.ReadAt(ctx, p, offset)
} }
@@ -42,24 +69,94 @@ func (r *sharedReadable) Size() int64 {
func (r *sharedReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { func (r *sharedReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
// TODO(radu): use a pool. // TODO(radu): use a pool.
return &sharedReadHandle{readable: r} rh := &sharedReadHandle{readable: r}
rh.readahead.state = makeReadaheadState(sharedMaxReadaheadSize)
return rh
} }
type sharedReadHandle struct { type sharedReadHandle struct {
readable *sharedReadable readable *sharedReadable
readahead struct {
state readaheadState
data []byte
offset int64
}
forCompaction bool
} }
var _ objstorage.ReadHandle = (*sharedReadHandle)(nil) var _ objstorage.ReadHandle = (*sharedReadHandle)(nil)
// ReadAt is part of the objstorage.ReadHandle interface.
func (r *sharedReadHandle) ReadAt(ctx context.Context, p []byte, offset int64) error { func (r *sharedReadHandle) ReadAt(ctx context.Context, p []byte, offset int64) error {
return r.readable.ReadAt(ctx, p, offset) readaheadSize := r.maybeReadahead(offset, len(p))
// Check if we already have the data from a previous read-ahead.
if rhSize := int64(len(r.readahead.data)); rhSize > 0 {
if r.readahead.offset <= offset && r.readahead.offset+rhSize > offset {
n := copy(p, r.readahead.data[offset-r.readahead.offset:])
if n == len(p) {
// All data was available.
return nil
}
// Use the data that we had and do a shorter read.
offset += int64(n)
p = p[n:]
readaheadSize -= n
}
}
if readaheadSize > len(p) {
// Don't try to read past EOF.
if offset+int64(readaheadSize) > r.readable.size {
readaheadSize = int(r.readable.size - offset)
if readaheadSize <= 0 {
// This shouldn't happen in practice (Pebble should never try to read
// past EOF).
return io.EOF
}
}
r.readahead.offset = offset
// TODO(radu): we need to somehow account for this memory.
if cap(r.readahead.data) >= readaheadSize {
r.readahead.data = r.readahead.data[:readaheadSize]
} else {
r.readahead.data = make([]byte, readaheadSize)
}
if err := r.readable.readInternal(ctx, r.readahead.data, offset, r.forCompaction); err != nil {
// Make sure we don't treat the data as valid next time.
r.readahead.data = r.readahead.data[:0]
return err
}
copy(p, r.readahead.data)
return nil
}
return r.readable.readInternal(ctx, p, offset, r.forCompaction)
} }
func (r *sharedReadHandle) maybeReadahead(offset int64, len int) int {
if r.forCompaction {
return sharedMaxReadaheadSize
}
return int(r.readahead.state.maybeReadahead(offset, int64(len)))
}
// Close is part of the objstorage.ReadHandle interface.
func (r *sharedReadHandle) Close() error { func (r *sharedReadHandle) Close() error {
r.readable = nil r.readable = nil
r.readahead.data = nil
return nil return nil
} }
func (r *sharedReadHandle) MaxReadahead() {} // SetupForCompaction is part of the objstorage.ReadHandle interface.
func (r *sharedReadHandle) SetupForCompaction() {
r.forCompaction = true
}
func (r *sharedReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {} // RecordCacheHit is part of the objstorage.ReadHandle interface.
func (r *sharedReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {
if !r.forCompaction {
r.readahead.state.recordCacheHit(offset, size)
}
}

View File

@@ -0,0 +1,570 @@
// Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package sharedcache
import (
"context"
"fmt"
"io"
"sync"
"sync/atomic"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/objstorage/shared"
"github.com/cockroachdb/pebble/vfs"
)
const (
shardingBlockSize = 1024 * 1024
)
// Cache is a persistent cache backed by a local filesystem. It is intended
// to cache data that is in slower shared storage (e.g. S3), hence the
// package name 'sharedcache'.
type Cache struct {
shards []shard
logger base.Logger
blockSize int
writeBackWaitGroup sync.WaitGroup
// TODO(josh): Have a dedicated metrics struct. Right now, this
// is just for testing.
misses atomic.Int32
}
// Open opens a cache. If there is no existing cache at fsDir, a new one
// is created.
func Open(
fs vfs.FS, logger base.Logger, fsDir string, blockSize int, sizeBytes int64, numShards int,
) (*Cache, error) {
min := shardingBlockSize * int64(numShards)
if sizeBytes < min {
return nil, errors.Errorf("cache size %d lower than min %d", sizeBytes, min)
}
sc := &Cache{
logger: logger,
blockSize: blockSize,
}
sc.shards = make([]shard, numShards)
blocksPerShard := sizeBytes / int64(numShards) / int64(blockSize)
for i := range sc.shards {
if err := sc.shards[i].init(fs, fsDir, i, blocksPerShard, blockSize); err != nil {
return nil, err
}
}
return sc, nil
}
// Close closes the cache. Methods such as ReadAt should not be called after Close is
// called.
func (c *Cache) Close() error {
c.writeBackWaitGroup.Wait()
var retErr error
for i := range c.shards {
if err := c.shards[i].close(); err != nil && retErr == nil {
retErr = err
}
}
c.shards = nil
return retErr
}
// ReadFlags contains options for Cache.ReadAt.
type ReadFlags struct {
// ReadOnly instructs ReadAt to not write any new data into the cache; it is
// used when the data is unlikely to be used again.
ReadOnly bool
}
// ReadAt performs a read form an object, attempting to use cached data when
// possible.
func (c *Cache) ReadAt(
ctx context.Context,
fileNum base.DiskFileNum,
p []byte,
ofs int64,
objReader shared.ObjectReader,
objSize int64,
flags ReadFlags,
) error {
if ofs >= objSize {
if invariants.Enabled {
panic("invalid ReadAt offset")
}
return io.EOF
}
// TODO(radu): for compaction reads, we may not want to read from the cache at
// all.
{
n, err := c.get(fileNum, p, ofs)
if err != nil {
return err
}
if n == len(p) {
// Everything was in cache!
return nil
}
// Note this. The below code does not need the original ofs, as with the earlier
// reading from the cache done, the relevant offset is ofs + int64(n). Same with p.
ofs += int64(n)
p = p[n:]
if invariants.Enabled {
if n != 0 && ofs%int64(c.blockSize) != 0 {
panic(fmt.Sprintf("after non-zero read from cache, ofs is not block-aligned: %v %v", ofs, n))
}
}
}
c.misses.Add(1)
if flags.ReadOnly {
return objReader.ReadAt(ctx, p, ofs)
}
// We must do reads with offset & size that are multiples of the block size. Else
// later cache hits may return incorrect zeroed results from the cache.
firstBlockInd := ofs / int64(c.blockSize)
adjustedOfs := firstBlockInd * int64(c.blockSize)
// Take the length of what is left to read plus the length of the adjustment of
// the offset plus the size of a block minus one and divide by the size of a block
// to get the number of blocks to read from the object.
sizeOfOffAdjustment := int(ofs - adjustedOfs)
numBlocksToRead := ((len(p) + sizeOfOffAdjustment) + (c.blockSize - 1)) / c.blockSize
adjustedLen := numBlocksToRead * c.blockSize
adjustedP := make([]byte, adjustedLen)
// Read the rest from the object. We may need to cap the length to avoid past EOF reads.
eofCap := int64(adjustedLen)
if adjustedOfs+eofCap > objSize {
eofCap = objSize - adjustedOfs
}
if err := objReader.ReadAt(ctx, adjustedP[:eofCap], adjustedOfs); err != nil {
return err
}
copy(p, adjustedP[sizeOfOffAdjustment:])
// TODO(josh): For writing back to the cache, we may want a concurrency-limited approach
// that does batching, instead of what is below. I think it's reasonable though to
// run a production experiment with what is below, before making adjustments. Note that
// the writes being done are in multiples of the filesystem block size. The filesystem
// might do okay with them.
c.writeBackWaitGroup.Add(1)
go func() {
defer c.writeBackWaitGroup.Done()
if err := c.set(fileNum, adjustedP, adjustedOfs); err != nil {
// TODO(josh): Would like to log at error severity, but base.Logger doesn't
// have error severity.
c.logger.Infof("writing back to cache after miss failed: %v", err)
}
}()
return nil
}
// get attempts to read the requested data from the cache, if it is already
// there.
//
// If all data is available, returns n = len(p).
//
// If data is partially available, a prefix of the data is read; returns n < len(p)
// and no error. If no prefix is available, returns n = 0 and no error.
func (c *Cache) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) {
// The data extent might cross shard boundaries, hence the loop. In the hot
// path, max two iterations of this loop will be executed, since reads are sized
// in units of sstable block size.
for {
shard := c.getShard(fileNum, ofs+int64(n))
cappedLen := len(p[n:])
if toBoundary := int(shardingBlockSize - ((ofs + int64(n)) % shardingBlockSize)); cappedLen > toBoundary {
cappedLen = toBoundary
}
numRead, err := shard.get(fileNum, p[n:n+cappedLen], ofs+int64(n))
if err != nil {
return n, err
}
n += numRead
if numRead < cappedLen {
// We only read a prefix from this shard.
return n, nil
}
if n == len(p) {
// We are done.
return n, nil
}
// Data extent crosses shard boundary, continue with next shard.
}
}
// set attempts to write the requested data to the cache. Both ofs & len(p) must
// be multiples of the block size.
//
// If all of p is not written to the shard, set returns a non-nil error.
func (c *Cache) set(fileNum base.DiskFileNum, p []byte, ofs int64) error {
if invariants.Enabled {
if ofs%int64(c.blockSize) != 0 || len(p)%c.blockSize != 0 {
panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p)))
}
}
// The data extent might cross shard boundaries, hence the loop. In the hot
// path, max two iterations of this loop will be executed, since reads are sized
// in units of sstable block size.
n := 0
for {
shard := c.getShard(fileNum, ofs+int64(n))
cappedLen := len(p[n:])
if toBoundary := int(shardingBlockSize - ((ofs + int64(n)) % shardingBlockSize)); cappedLen > toBoundary {
cappedLen = toBoundary
}
err := shard.set(fileNum, p[n:n+cappedLen], ofs+int64(n))
if err != nil {
return err
}
// set returns an error if cappedLen bytes aren't written to the shard.
n += cappedLen
if n == len(p) {
// We are done.
return nil
}
// Data extent crosses shard boundary, continue with next shard.
}
}
func (c *Cache) getShard(fileNum base.DiskFileNum, ofs int64) *shard {
const prime64 = 1099511628211
hash := uint64(fileNum.FileNum())*prime64 + uint64(ofs)/shardingBlockSize
// TODO(josh): Instance change ops are often run in production. Such an operation
// updates len(c.shards); see openSharedCache. As a result, the behavior of this
// function changes, and the cache empties out at restart time. We may want a better
// story here eventually.
return &c.shards[hash%uint64(len(c.shards))]
}
type shard struct {
file vfs.File
sizeInBlocks int64
blockSize int
mu struct {
sync.Mutex
// TODO(josh): None of these datastructures are space-efficient.
// Focusing on correctness to start.
where whereMap
locks []lockState
free []cacheBlockIndex
}
}
// Maps a logical block in an SST to an index of the cache block with the
// file contents (to the "cache block index").
type whereMap map[logicalBlockID]cacheBlockIndex
type logicalBlockID struct {
filenum base.DiskFileNum
// Offset into the file, in units of shard.BlockSize.
offsetInUnitsOfBlocks int64
}
type cacheBlockIndex int64
type lockState int64
const (
unlocked lockState = 0
// >0 lockState tracks the number of distinct readers of some cache block / logical block
// which is in the secondary cache. It is used to ensure that a cache block is not evicted
// and overwritten, while there are active readers.
readLockTakenInc = 1
// -1 lockState indicates that some cache block is currently being populated with data from
// blob storage. It is used to ensure that a cache block is not read or evicted again, while
// it is being populated.
writeLockTaken = -1
)
func (s *shard) init(
fs vfs.FS, fsDir string, shardIdx int, sizeInBlocks int64, blockSize int,
) error {
*s = shard{
sizeInBlocks: sizeInBlocks,
}
if blockSize < 1024 || shardingBlockSize%blockSize != 0 {
return errors.Newf("invalid block size %d (must divide %d)", blockSize, shardingBlockSize)
}
s.blockSize = blockSize
file, err := fs.OpenReadWrite(fs.PathJoin(fsDir, fmt.Sprintf("SHARED-CACHE-%03d", shardIdx)))
if err != nil {
return err
}
// TODO(radu): truncate file if necessary (especially important if we restart
// with more shards).
if err := file.Preallocate(0, int64(blockSize)*sizeInBlocks); err != nil {
return err
}
s.file = file
// TODO(josh): Right now, the secondary cache is not persistent. All existing
// cache contents will be over-written, since all metadata is only stored in
// memory.
s.mu.where = make(whereMap)
for i := int64(0); i < sizeInBlocks; i++ {
s.mu.locks = append(s.mu.locks, unlocked)
s.mu.free = append(s.mu.free, cacheBlockIndex(i))
}
return nil
}
func (s *shard) close() error {
defer func() {
s.file = nil
}()
return s.file.Close()
}
// get attempts to read the requested data from the shard. The data must not
// cross a shard boundary.
//
// If all data is available, returns n = len(p).
//
// If data is partially available, a prefix of the data is read; returns n < len(p)
// and no error. If no prefix is available, returns n = 0 and no error.
//
// TODO(josh): Today, if there are two cache blocks needed to satisfy a read, and the
// first block is not in the cache and the second one is, we will read both from
// blob storage. We should fix this. This is not an unlikely scenario if we are doing
// a reverse scan, since those iterate over sstable blocks in reverse order and due to
// cache block aligned reads will have read the suffix of the sstable block that will
// be needed next.
func (s *shard) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) {
if invariants.Enabled {
if ofs/shardingBlockSize != (ofs+int64(len(p))-1)/shardingBlockSize {
panic(fmt.Sprintf("get crosses shard boundary: %v %v", ofs, len(p)))
}
s.assertShardStateIsConsistent()
}
// The data extent might cross cache block boundaries, hence the loop. In the hot
// path, max two iterations of this loop will be executed, since reads are sized
// in units of sstable block size.
for {
k := logicalBlockID{
filenum: fileNum,
offsetInUnitsOfBlocks: (ofs + int64(n)) / int64(s.blockSize),
}
s.mu.Lock()
cacheBlockInd, ok := s.mu.where[k]
// TODO(josh): Multiple reads within the same few milliseconds (anything that is smaller
// than blob storage read latency) that miss on the same logical block ID will not necessarily
// be rare. We may want to do only one read, with the later readers blocking on the first read
// completing. This could be implemented either here or in the primary block cache. See
// https://github.com/cockroachdb/pebble/pull/2586 for additional discussion.
if !ok {
s.mu.Unlock()
return n, nil
}
if s.mu.locks[cacheBlockInd] == writeLockTaken {
// In practice, if we have two reads of the same SST block in close succession, we
// would expect the second to hit in the in-memory block cache. So it's not worth
// optimizing this case here.
s.mu.Unlock()
return n, nil
}
s.mu.locks[cacheBlockInd] += readLockTakenInc
s.mu.Unlock()
readAt := int64(cacheBlockInd) * int64(s.blockSize)
if n == 0 { // if first read
readAt += ofs % int64(s.blockSize)
}
readSize := s.blockSize
if n == 0 { // if first read
// Cast to int safe since ofs is modded by block size.
readSize -= int(ofs % int64(s.blockSize))
}
if len(p[n:]) <= readSize {
numRead, err := s.file.ReadAt(p[n:], readAt)
s.dropReadLock(cacheBlockInd)
return n + numRead, err
}
numRead, err := s.file.ReadAt(p[n:n+readSize], readAt)
s.dropReadLock(cacheBlockInd)
if err != nil {
return 0, err
}
// Note that numRead == readSize, since we checked for an error above.
n += numRead
}
}
// set attempts to write the requested data to the shard. The data must not
// cross a shard boundary, and both ofs & len(p) must be multiples of the
// block size.
//
// If all of p is not written to the shard, set returns a non-nil error.
func (s *shard) set(fileNum base.DiskFileNum, p []byte, ofs int64) error {
if invariants.Enabled {
if ofs/shardingBlockSize != (ofs+int64(len(p))-1)/shardingBlockSize {
panic(fmt.Sprintf("set crosses shard boundary: %v %v", ofs, len(p)))
}
if ofs%int64(s.blockSize) != 0 || len(p)%s.blockSize != 0 {
panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p)))
}
s.assertShardStateIsConsistent()
}
// The data extent might cross cache block boundaries, hence the loop. In the hot
// path, max two iterations of this loop will be executed, since reads are sized
// in units of sstable block size.
n := 0
for {
if n == len(p) {
return nil
}
if invariants.Enabled {
if n > len(p) {
panic(fmt.Sprintf("set with n greater than len(p): %v %v", n, len(p)))
}
}
// If the logical block is already in the cache, we should skip doing a set.
k := logicalBlockID{
filenum: fileNum,
offsetInUnitsOfBlocks: (ofs + int64(n)) / int64(s.blockSize),
}
s.mu.Lock()
if _, ok := s.mu.where[k]; ok {
s.mu.Unlock()
n += s.blockSize
continue
}
// Determine cache block index by looking at the free list and randomly evicting if
// it is empty.
var cacheBlockInd cacheBlockIndex
if len(s.mu.free) == 0 {
// TODO(josh): Right now, we do random eviction. Eventually, we will do something
// more sophisticated, e.g. leverage ClockPro.
foundBlockToEvict := false
var k logicalBlockID
for k1, v := range s.mu.where {
// This implies that hot blocks will not be evicted as often, meaning the
// eviction scheme is not quite random. I think that's fine for now, since we plan
// to leverage a more sophisticated eviction scheme eventually anyway.
lock := s.mu.locks[v]
if lock >= readLockTakenInc || lock == writeLockTaken {
continue
}
foundBlockToEvict = true
cacheBlockInd = v
k = k1
break
}
// TODO(josh): We may want to block until a block frees up, instead of returning
// an error here. But I think we can do that later on, e.g. after running some production
// experiments.
if !foundBlockToEvict {
s.mu.Unlock()
return errors.New("no block to evict so skipping write to cache")
}
delete(s.mu.where, k)
} else {
cacheBlockInd = s.mu.free[len(s.mu.free)-1]
s.mu.free = s.mu.free[:len(s.mu.free)-1]
}
s.mu.where[k] = cacheBlockInd
s.mu.locks[cacheBlockInd] = writeLockTaken
s.mu.Unlock()
writeAt := int64(cacheBlockInd) * int64(s.blockSize)
writeSize := s.blockSize
if len(p[n:]) <= writeSize {
writeSize = len(p[n:])
}
_, err := s.file.WriteAt(p[n:n+writeSize], writeAt)
if err != nil {
s.freeBlock(k, cacheBlockInd)
return err
}
s.dropWriteLock(cacheBlockInd)
n += writeSize
}
}
// Doesn't inline currently. This might be okay, but something to keep in mind.
func (s *shard) dropReadLock(cacheBlockInd cacheBlockIndex) {
s.mu.Lock()
s.mu.locks[cacheBlockInd] -= readLockTakenInc
if invariants.Enabled {
if s.mu.locks[cacheBlockInd] < 0 {
panic(fmt.Sprintf("unexpected lock state %v in dropReadLock: %v", s.mu.locks[cacheBlockInd], s.mu.locks))
}
}
s.mu.Unlock()
}
// Doesn't inline currently. This might be okay, but something to keep in mind.
func (s *shard) dropWriteLock(cacheBlockInd cacheBlockIndex) {
s.mu.Lock()
if invariants.Enabled {
if s.mu.locks[cacheBlockInd] != writeLockTaken {
panic(fmt.Sprintf("unexpected lock state %v in dropWriteLock: %v", s.mu.locks[cacheBlockInd], s.mu.locks))
}
}
s.mu.locks[cacheBlockInd] = unlocked
s.mu.Unlock()
}
// Doesn't inline currently. This might be okay, but something to keep in mind.
func (s *shard) freeBlock(k logicalBlockID, cacheBlockInd cacheBlockIndex) {
s.mu.Lock()
delete(s.mu.where, k)
s.mu.locks[cacheBlockInd] = unlocked
s.mu.free = append(s.mu.free, cacheBlockInd)
s.mu.Unlock()
}
func (s *shard) assertShardStateIsConsistent() {
s.mu.Lock()
defer s.mu.Unlock()
cacheBlockInds := make(map[cacheBlockIndex]bool)
for _, v := range s.mu.where {
if cacheBlockInds[v] {
panic(fmt.Sprintf("repeated cache block index (where): %v %v %v %v", v, cacheBlockInds, s.mu.where, s.mu.free))
}
cacheBlockInds[v] = true
}
for _, v := range s.mu.free {
if cacheBlockInds[v] {
panic(fmt.Sprintf("repeated cache block index (free): %v %v %v %v", v, cacheBlockInds, s.mu.where, s.mu.free))
}
cacheBlockInds[v] = true
}
for i := int64(0); i < s.sizeInBlocks; i++ {
if !cacheBlockInds[cacheBlockIndex(i)] {
panic(fmt.Sprintf("missing cache block index: %v %v %v %v", i, cacheBlockInds, s.mu.where, s.mu.free))
}
}
if int64(len(s.mu.locks)) != s.sizeInBlocks {
panic(fmt.Sprintf("lock table isn't correct size: %v %v", len(s.mu.locks), s.sizeInBlocks))
}
for _, ls := range s.mu.locks {
if ls < writeLockTaken {
panic(fmt.Sprintf("lock state %v is not allowed: %v", ls, s.mu.locks))
}
}
}

View File

@@ -15,6 +15,8 @@ import (
"github.com/cockroachdb/pebble/vfs" "github.com/cockroachdb/pebble/vfs"
) )
const fileMaxReadaheadSize = 256 * 1024 /* 256KB */
// fileReadable implements objstorage.Readable on top of a vfs.File. // fileReadable implements objstorage.Readable on top of a vfs.File.
// //
// The implementation might use Prealloc and might reopen the file with // The implementation might use Prealloc and might reopen the file with
@@ -75,7 +77,7 @@ func (r *fileReadable) Size() int64 {
func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
rh := readHandlePool.Get().(*vfsReadHandle) rh := readHandlePool.Get().(*vfsReadHandle)
rh.r = r rh.r = r
rh.rs = makeReadaheadState() rh.rs = makeReadaheadState(fileMaxReadaheadSize)
return rh return rh
} }
@@ -126,10 +128,10 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
n, err = rh.sequentialFile.ReadAt(p, offset) n, err = rh.sequentialFile.ReadAt(p, offset)
} else { } else {
if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 { if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 {
if readaheadSize >= maxReadaheadSize { if readaheadSize >= fileMaxReadaheadSize {
// We've reached the maximum readahead size. Beyond this point, rely on // We've reached the maximum readahead size. Beyond this point, rely on
// OS-level readahead. // OS-level readahead.
rh.MaxReadahead() rh.switchToOSReadahead()
} else { } else {
_ = rh.r.file.Prefetch(offset, readaheadSize) _ = rh.r.file.Prefetch(offset, readaheadSize)
} }
@@ -142,8 +144,12 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
return err return err
} }
// MaxReadahead is part of the objstorage.ReadHandle interface. // SetupForCompaction is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) MaxReadahead() { func (rh *vfsReadHandle) SetupForCompaction() {
rh.switchToOSReadahead()
}
func (rh *vfsReadHandle) switchToOSReadahead() {
if rh.sequentialFile != nil { if rh.sequentialFile != nil {
return return
} }

View File

@@ -66,8 +66,11 @@ type Storage interface {
type ObjectReader interface { type ObjectReader interface {
// ReadAt reads len(p) bytes into p starting at offset off. // ReadAt reads len(p) bytes into p starting at offset off.
// //
// Clients of ReadAt can execute parallel ReadAt calls on the // Does not return partial results; if offset + len(p) is past the end of the
// same ObjectReader. // object, an error is returned.
//
// Clients of ReadAt can execute parallel ReadAt calls on the same
// ObjectReader.
ReadAt(ctx context.Context, p []byte, offset int64) error ReadAt(ctx context.Context, p []byte, offset int64) error
Close() error Close() error

View File

@@ -210,8 +210,8 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
write: d.commitWrite, write: d.commitWrite,
}) })
d.deletionLimiter = rate.NewLimiter( d.deletionLimiter = rate.NewLimiter(
rate.Limit(d.opts.Experimental.MinDeletionRate), rate.Limit(d.opts.TargetByteDeletionRate),
d.opts.Experimental.MinDeletionRate) d.opts.TargetByteDeletionRate)
d.mu.nextJobID = 1 d.mu.nextJobID = 1
d.mu.mem.nextSize = opts.MemTableSize d.mu.mem.nextSize = opts.MemTableSize
if d.mu.mem.nextSize > initialMemTableSize { if d.mu.mem.nextSize > initialMemTableSize {
@@ -512,7 +512,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
} }
d.mu.tableStats.cond.L = &d.mu.Mutex d.mu.tableStats.cond.L = &d.mu.Mutex
d.mu.tableValidation.cond.L = &d.mu.Mutex d.mu.tableValidation.cond.L = &d.mu.Mutex
if !d.opts.ReadOnly && !d.opts.private.disableTableStats { if !d.opts.ReadOnly {
d.maybeCollectTableStatsLocked() d.maybeCollectTableStatsLocked()
} }
d.calculateDiskAvailableBytes() d.calculateDiskAvailableBytes()
@@ -804,13 +804,12 @@ func (d *DB) replayWAL(
paths[i] = base.MakeFilepath(d.opts.FS, d.dirname, fileTypeTable, n) paths[i] = base.MakeFilepath(d.opts.FS, d.dirname, fileTypeTable, n)
} }
var meta []*manifest.FileMetadata var lr ingestLoadResult
meta, _, err = ingestLoad( lr, err = ingestLoad(d.opts, d.mu.formatVers.vers, paths, nil, d.cacheID, fileNums)
d.opts, d.mu.formatVers.vers, paths, d.cacheID, fileNums,
)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
meta := lr.localMeta
if uint32(len(meta)) != b.Count() { if uint32(len(meta)) != b.Count() {
panic("pebble: couldn't load all files in WAL entry.") panic("pebble: couldn't load all files in WAL entry.")

View File

@@ -17,6 +17,7 @@ import (
"github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/cache"
"github.com/cockroachdb/pebble/internal/humanize" "github.com/cockroachdb/pebble/internal/humanize"
"github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/objstorage/shared" "github.com/cockroachdb/pebble/objstorage/shared"
"github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/pebble/sstable"
@@ -118,10 +119,13 @@ type IterOptions struct {
// function can be used by multiple iterators, if the iterator is cloned. // function can be used by multiple iterators, if the iterator is cloned.
TableFilter func(userProps map[string]string) bool TableFilter func(userProps map[string]string) bool
// PointKeyFilters can be used to avoid scanning tables and blocks in tables // PointKeyFilters can be used to avoid scanning tables and blocks in tables
// when iterating over point keys. It is requires that this slice is sorted in // when iterating over point keys. This slice represents an intersection
// increasing order of the BlockPropertyFilter.ShortID. This slice represents // across all filters, i.e., all filters must indicate that the block is
// an intersection across all filters, i.e., all filters must indicate that the // relevant.
// block is relevant. //
// Performance note: When len(PointKeyFilters) > 0, the caller should ensure
// that cap(PointKeyFilters) is at least len(PointKeyFilters)+1. This helps
// avoid allocations in Pebble internal code that mutates the slice.
PointKeyFilters []BlockPropertyFilter PointKeyFilters []BlockPropertyFilter
// RangeKeyFilters can be usefd to avoid scanning tables and blocks in tables // RangeKeyFilters can be usefd to avoid scanning tables and blocks in tables
// when iterating over range keys. The same requirements that apply to // when iterating over range keys. The same requirements that apply to
@@ -180,6 +184,10 @@ type IterOptions struct {
level manifest.Level level manifest.Level
// disableLazyCombinedIteration is an internal testing option. // disableLazyCombinedIteration is an internal testing option.
disableLazyCombinedIteration bool disableLazyCombinedIteration bool
// snapshotForHideObsoletePoints is specified for/by levelIter when opening
// files and is used to decide whether to hide obsolete points. A value of 0
// implies obsolete points should not be hidden.
snapshotForHideObsoletePoints uint64
// NB: If adding new Options, you must account for them in iterator // NB: If adding new Options, you must account for them in iterator
// construction and Iterator.SetOptions. // construction and Iterator.SetOptions.
@@ -222,6 +230,17 @@ func (o *IterOptions) getLogger() Logger {
return o.logger return o.logger
} }
// SpanIterOptions creates a SpanIterOptions from this IterOptions.
func (o *IterOptions) SpanIterOptions(level manifest.Level) keyspan.SpanIterOptions {
if o == nil {
return keyspan.SpanIterOptions{Level: level}
}
return keyspan.SpanIterOptions{
RangeKeyFilters: o.RangeKeyFilters,
Level: level,
}
}
// scanInternalOptions is similar to IterOptions, meant for use with // scanInternalOptions is similar to IterOptions, meant for use with
// scanInternalIterator. // scanInternalIterator.
type scanInternalOptions struct { type scanInternalOptions struct {
@@ -509,17 +528,6 @@ type Options struct {
// concurrency slots as determined by the two options is chosen. // concurrency slots as determined by the two options is chosen.
CompactionDebtConcurrency int CompactionDebtConcurrency int
// MinDeletionRate is the minimum number of bytes per second that would
// be deleted. Deletion pacing is used to slow down deletions when
// compactions finish up or readers close, and newly-obsolete files need
// cleaning up. Deleting lots of files at once can cause disk latency to
// go up on some SSDs, which this functionality guards against. This is a
// minimum as the maximum is theoretically unlimited; pacing is disabled
// when there are too many obsolete files relative to live bytes, or
// there isn't enough disk space available. Setting this to 0 disables
// deletion pacing, which is also the default.
MinDeletionRate int
// ReadCompactionRate controls the frequency of read triggered // ReadCompactionRate controls the frequency of read triggered
// compactions by adjusting `AllowedSeeks` in manifest.FileMetadata: // compactions by adjusting `AllowedSeeks` in manifest.FileMetadata:
// //
@@ -606,12 +614,6 @@ type Options struct {
// for CPUWorkPermissionGranter for more details. // for CPUWorkPermissionGranter for more details.
CPUWorkPermissionGranter CPUWorkPermissionGranter CPUWorkPermissionGranter CPUWorkPermissionGranter
// PointTombstoneWeight is a float in the range [0, +inf) used to weight the
// point tombstone heuristics during compaction picking.
//
// The default value is 1, which results in no scaling of point tombstones.
PointTombstoneWeight float64
// EnableValueBlocks is used to decide whether to enable writing // EnableValueBlocks is used to decide whether to enable writing
// TableFormatPebblev3 sstables. This setting is only respected by a // TableFormatPebblev3 sstables. This setting is only respected by a
// specific subset of format major versions: FormatSSTableValueBlocks, // specific subset of format major versions: FormatSSTableValueBlocks,
@@ -626,18 +628,24 @@ type Options struct {
ShortAttributeExtractor ShortAttributeExtractor ShortAttributeExtractor ShortAttributeExtractor
// RequiredInPlaceValueBound specifies an optional span of user key // RequiredInPlaceValueBound specifies an optional span of user key
// prefixes for which the values must be stored with the key. This is // prefixes that are not-MVCC, but have a suffix. For these the values
// useful for statically known exclusions to value separation. In // must be stored with the key, since the concept of "older versions" is
// CockroachDB, this will be used for the lock table key space that has // not defined. It is also useful for statically known exclusions to value
// non-empty suffixes, but those locks don't represent actual MVCC // separation. In CockroachDB, this will be used for the lock table key
// versions (the suffix ordering is arbitrary). We will also need to add // space that has non-empty suffixes, but those locks don't represent
// support for dynamically configured exclusions (we want the default to // actual MVCC versions (the suffix ordering is arbitrary). We will also
// be to allow Pebble to decide whether to separate the value or not, // need to add support for dynamically configured exclusions (we want the
// hence this is structured as exclusions), for example, for users of // default to be to allow Pebble to decide whether to separate the value
// CockroachDB to dynamically exclude certain tables. // or not, hence this is structured as exclusions), for example, for users
// of CockroachDB to dynamically exclude certain tables.
// //
// Any change in exclusion behavior takes effect only on future written // Any change in exclusion behavior takes effect only on future written
// sstables, and does not start rewriting existing sstables. // sstables, and does not start rewriting existing sstables.
//
// Even ignoring changes in this setting, exclusions are interpreted as a
// guidance by Pebble, and not necessarily honored. Specifically, user
// keys with multiple Pebble-versions *may* have the older versions stored
// in value blocks.
RequiredInPlaceValueBound UserKeyPrefixBound RequiredInPlaceValueBound UserKeyPrefixBound
// DisableIngestAsFlushable disables lazy ingestion of sstables through // DisableIngestAsFlushable disables lazy ingestion of sstables through
@@ -858,6 +866,19 @@ type Options struct {
// changing options dynamically? // changing options dynamically?
WALMinSyncInterval func() time.Duration WALMinSyncInterval func() time.Duration
// TargetByteDeletionRate is the rate (in bytes per second) at which sstable file
// deletions are limited to (under normal circumstances).
//
// Deletion pacing is used to slow down deletions when compactions finish up
// or readers close and newly-obsolete files need cleaning up. Deleting lots
// of files at once can cause disk latency to go up on some SSDs, which this
// functionality guards against. This is only a best-effort target; pacing is
// disabled when there are too many obsolete files relative to live bytes, or
// there isn't enough disk space available.
//
// Setting this to 0 disables deletion pacing, which is also the default.
TargetByteDeletionRate int
// private options are only used by internal tests or are used internally // private options are only used by internal tests or are used internally
// for facilitating upgrade paths of unconfigurable functionality. // for facilitating upgrade paths of unconfigurable functionality.
private struct { private struct {
@@ -1043,10 +1064,6 @@ func (o *Options) EnsureDefaults() *Options {
if o.Experimental.CPUWorkPermissionGranter == nil { if o.Experimental.CPUWorkPermissionGranter == nil {
o.Experimental.CPUWorkPermissionGranter = defaultCPUWorkGranter{} o.Experimental.CPUWorkPermissionGranter = defaultCPUWorkGranter{}
} }
if o.Experimental.PointTombstoneWeight == 0 {
o.Experimental.PointTombstoneWeight = 1
}
if o.Experimental.MultiLevelCompactionHueristic == nil { if o.Experimental.MultiLevelCompactionHueristic == nil {
o.Experimental.MultiLevelCompactionHueristic = NoMultiLevel{} o.Experimental.MultiLevelCompactionHueristic = NoMultiLevel{}
} }
@@ -1167,9 +1184,8 @@ func (o *Options) String() string {
fmt.Fprintf(&buf, " max_open_files=%d\n", o.MaxOpenFiles) fmt.Fprintf(&buf, " max_open_files=%d\n", o.MaxOpenFiles)
fmt.Fprintf(&buf, " mem_table_size=%d\n", o.MemTableSize) fmt.Fprintf(&buf, " mem_table_size=%d\n", o.MemTableSize)
fmt.Fprintf(&buf, " mem_table_stop_writes_threshold=%d\n", o.MemTableStopWritesThreshold) fmt.Fprintf(&buf, " mem_table_stop_writes_threshold=%d\n", o.MemTableStopWritesThreshold)
fmt.Fprintf(&buf, " min_deletion_rate=%d\n", o.Experimental.MinDeletionRate) fmt.Fprintf(&buf, " min_deletion_rate=%d\n", o.TargetByteDeletionRate)
fmt.Fprintf(&buf, " merger=%s\n", o.Merger.Name) fmt.Fprintf(&buf, " merger=%s\n", o.Merger.Name)
fmt.Fprintf(&buf, " point_tombstone_weight=%f\n", o.Experimental.PointTombstoneWeight)
fmt.Fprintf(&buf, " read_compaction_rate=%d\n", o.Experimental.ReadCompactionRate) fmt.Fprintf(&buf, " read_compaction_rate=%d\n", o.Experimental.ReadCompactionRate)
fmt.Fprintf(&buf, " read_sampling_multiplier=%d\n", o.Experimental.ReadSamplingMultiplier) fmt.Fprintf(&buf, " read_sampling_multiplier=%d\n", o.Experimental.ReadSamplingMultiplier)
fmt.Fprintf(&buf, " strict_wal_tail=%t\n", o.private.strictWALTail) fmt.Fprintf(&buf, " strict_wal_tail=%t\n", o.private.strictWALTail)
@@ -1374,7 +1390,7 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
// version is valid right here. // version is valid right here.
var v uint64 var v uint64
v, err = strconv.ParseUint(value, 10, 64) v, err = strconv.ParseUint(value, 10, 64)
if vers := FormatMajorVersion(v); vers > FormatNewest || vers == FormatDefault { if vers := FormatMajorVersion(v); vers > internalFormatNewest || vers == FormatDefault {
err = errors.Newf("unknown format major version %d", o.FormatMajorVersion) err = errors.Newf("unknown format major version %d", o.FormatMajorVersion)
} }
if err == nil { if err == nil {
@@ -1414,12 +1430,12 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
// Do nothing; option existed in older versions of pebble, and // Do nothing; option existed in older versions of pebble, and
// may be meaningful again eventually. // may be meaningful again eventually.
case "min_deletion_rate": case "min_deletion_rate":
o.Experimental.MinDeletionRate, err = strconv.Atoi(value) o.TargetByteDeletionRate, err = strconv.Atoi(value)
case "min_flush_rate": case "min_flush_rate":
// Do nothing; option existed in older versions of pebble, and // Do nothing; option existed in older versions of pebble, and
// may be meaningful again eventually. // may be meaningful again eventually.
case "point_tombstone_weight": case "point_tombstone_weight":
o.Experimental.PointTombstoneWeight, err = strconv.ParseFloat(value, 64) // Do nothing; deprecated.
case "strict_wal_tail": case "strict_wal_tail":
o.private.strictWALTail, err = strconv.ParseBool(value) o.private.strictWALTail, err = strconv.ParseBool(value)
case "merger": case "merger":
@@ -1594,9 +1610,9 @@ func (o *Options) Validate() error {
fmt.Fprintf(&buf, "MemTableStopWritesThreshold (%d) must be >= 2\n", fmt.Fprintf(&buf, "MemTableStopWritesThreshold (%d) must be >= 2\n",
o.MemTableStopWritesThreshold) o.MemTableStopWritesThreshold)
} }
if o.FormatMajorVersion > FormatNewest { if o.FormatMajorVersion > internalFormatNewest {
fmt.Fprintf(&buf, "FormatMajorVersion (%d) must be <= %d\n", fmt.Fprintf(&buf, "FormatMajorVersion (%d) must be <= %d\n",
o.FormatMajorVersion, FormatNewest) o.FormatMajorVersion, internalFormatNewest)
} }
if o.TableCache != nil && o.Cache != o.TableCache.cache { if o.TableCache != nil && o.Cache != o.TableCache.cache {
fmt.Fprintf(&buf, "underlying cache in the TableCache and the Cache dont match\n") fmt.Fprintf(&buf, "underlying cache in the TableCache and the Cache dont match\n")
@@ -1616,6 +1632,7 @@ func (o *Options) MakeReaderOptions() sstable.ReaderOptions {
readerOpts.Comparer = o.Comparer readerOpts.Comparer = o.Comparer
readerOpts.Filters = o.Filters readerOpts.Filters = o.Filters
if o.Merger != nil { if o.Merger != nil {
readerOpts.Merge = o.Merger.Merge
readerOpts.MergerName = o.Merger.Name readerOpts.MergerName = o.Merger.Name
} }
readerOpts.LoggerAndTracer = o.LoggerAndTracer readerOpts.LoggerAndTracer = o.LoggerAndTracer
@@ -1640,6 +1657,9 @@ func (o *Options) MakeWriterOptions(level int, format sstable.TableFormat) sstab
if format >= sstable.TableFormatPebblev3 { if format >= sstable.TableFormatPebblev3 {
writerOpts.ShortAttributeExtractor = o.Experimental.ShortAttributeExtractor writerOpts.ShortAttributeExtractor = o.Experimental.ShortAttributeExtractor
writerOpts.RequiredInPlaceValueBound = o.Experimental.RequiredInPlaceValueBound writerOpts.RequiredInPlaceValueBound = o.Experimental.RequiredInPlaceValueBound
if format >= sstable.TableFormatPebblev4 && level == numLevels-1 {
writerOpts.WritingToLowestLevel = true
}
} }
levelOpts := o.Level(level) levelOpts := o.Level(level)
writerOpts.BlockRestartInterval = levelOpts.BlockRestartInterval writerOpts.BlockRestartInterval = levelOpts.BlockRestartInterval

View File

@@ -106,7 +106,7 @@ func (p *deletionPacer) limit(amount uint64, info deletionPacerInfo) error {
} }
// maybeThrottle slows down a deletion of this file if it's faster than // maybeThrottle slows down a deletion of this file if it's faster than
// opts.Experimental.MinDeletionRate. // opts.TargetByteDeletionRate.
func (p *deletionPacer) maybeThrottle(bytesToDelete uint64) error { func (p *deletionPacer) maybeThrottle(bytesToDelete uint64) error {
return p.limit(bytesToDelete, p.getInfo()) return p.limit(bytesToDelete, p.getInfo())
} }

View File

@@ -63,8 +63,7 @@ func (i *Iterator) constructRangeKeyIter() {
// around Key Trailer order. // around Key Trailer order.
iter := current.RangeKeyLevels[0].Iter() iter := current.RangeKeyLevels[0].Iter()
for f := iter.Last(); f != nil; f = iter.Prev() { for f := iter.Last(); f != nil; f = iter.Prev() {
spanIterOpts := &keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} spanIter, err := i.newIterRangeKey(f, i.opts.SpanIterOptions(manifest.Level(0)))
spanIter, err := i.newIterRangeKey(f, spanIterOpts)
if err != nil { if err != nil {
i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err}) i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err})
continue continue
@@ -78,7 +77,7 @@ func (i *Iterator) constructRangeKeyIter() {
continue continue
} }
li := i.rangeKey.iterConfig.NewLevelIter() li := i.rangeKey.iterConfig.NewLevelIter()
spanIterOpts := keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} spanIterOpts := i.opts.SpanIterOptions(manifest.Level(level))
li.Init(spanIterOpts, i.cmp, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(), li.Init(spanIterOpts, i.cmp, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(),
manifest.Level(level), manifest.KeyTypeRange) manifest.Level(level), manifest.KeyTypeRange)
i.rangeKey.iterConfig.AddLevel(li) i.rangeKey.iterConfig.AddLevel(li)

View File

@@ -7,12 +7,15 @@ package pebble
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/cockroachdb/errors" "github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/objstorage"
"github.com/cockroachdb/pebble/sstable"
) )
const ( const (
@@ -92,8 +95,41 @@ type pcIterPos int
const ( const (
pcIterPosCur pcIterPos = iota pcIterPosCur pcIterPos = iota
pcIterPosNext pcIterPosNext
pcIterPosPrev
) )
// pointCollapsingSSTIterator implements sstable.Iterator while composing
// pointCollapsingIterator.
type pointCollapsingSSTIterator struct {
pointCollapsingIterator
childIter sstable.Iterator
}
var pcSSTIterPool = sync.Pool{
New: func() interface{} {
return &pointCollapsingSSTIterator{}
},
}
// MaybeFilteredKeys implements the sstable.Iterator interface.
func (p *pointCollapsingSSTIterator) MaybeFilteredKeys() bool {
return p.childIter.MaybeFilteredKeys()
}
// SetCloseHook implements the sstable.Iterator interface.
func (p *pointCollapsingSSTIterator) SetCloseHook(fn func(i sstable.Iterator) error) {
p.childIter.SetCloseHook(fn)
}
// Close implements the sstable.Iterator interface.
func (p *pointCollapsingSSTIterator) Close() error {
err := p.pointCollapsingIterator.Close()
p.pointCollapsingIterator = pointCollapsingIterator{}
p.childIter = nil
pcSSTIterPool.Put(p)
return err
}
// pointCollapsingIterator is an internalIterator that collapses point keys and // pointCollapsingIterator is an internalIterator that collapses point keys and
// returns at most one point internal key for each user key. Merges are merged, // returns at most one point internal key for each user key. Merges are merged,
// sets are emitted as-is, and SingleDeletes are collapsed with the next point // sets are emitted as-is, and SingleDeletes are collapsed with the next point
@@ -112,32 +148,52 @@ type pointCollapsingIterator struct {
merge base.Merge merge base.Merge
err error err error
seqNum uint64 seqNum uint64
// The current position of `iter`. Could be backed by savedKey (i.e. iterKey == // The current position of `iter`. Always owned by the underlying iter.
// &savedKey) or could be owned by `iter`. findNextEntry and similar methods iterKey *InternalKey
// are expected to save the current value of this to savedKey if they're // The last saved key. findNextEntry and similar methods are expected to save
// iterating away from the current key but still need to retain it. See // the current value of iterKey to savedKey if they're iterating away from the
// comments in findNextEntry on how this field is used. // current key but still need to retain it. See comments in findNextEntry on
// how this field is used.
// //
// At the end of a positioning call: // At the end of a positioning call:
// - if pos == pcIterPosNext, iterKey is pointing to the next user key owned // - if pos == pcIterPosNext, iterKey is pointing to the next user key owned
// by `iter` while savedKey is holding a copy to our current key. // by `iter` while savedKey is holding a copy to our current key.
// - If pos == pcIterPosCur, iterKey is pointing to either a savedKey-backed // - If pos == pcIterPosCur, iterKey is pointing to an `iter`-owned current
// copy of the current key, or an `iter`-owned current key in which case // key, and savedKey is either undefined or pointing to a version of the
// the value of savedKey is undefined. // current key owned by this iterator (i.e. backed by savedKeyBuf).
iterKey *InternalKey // - if pos == pcIterPosPrev, iterKey is pointing to the key before
// p.savedKey. p.savedKey is treated as the current key and is owned by
// this iterator, while p.iterKey is the previous key that is the current
// position of the child iterator.
savedKey InternalKey savedKey InternalKey
savedKeyBuf []byte
// elideRangeDeletes ignores range deletes returned by the interleaving
// iterator if true.
elideRangeDeletes bool
// Value at the current iterator position, at iterKey. // Value at the current iterator position, at iterKey.
value base.LazyValue iterValue base.LazyValue
// Saved value backed by valueBuf, if set. Used in reverse iteration, and
// for merges.
savedValue base.LazyValue
// Used for Merge keys only. // Used for Merge keys only.
valueMerger ValueMerger valueMerger ValueMerger
valueBuf []byte valueBuf []byte
// If fixedSeqNum is non-zero, all emitted points are verified to have this
// fixed sequence number.
fixedSeqNum uint64
} }
// SeekPrefixGE implements the InternalIterator interface. // SeekPrefixGE implements the InternalIterator interface.
func (p *pointCollapsingIterator) SeekPrefixGE( func (p *pointCollapsingIterator) SeekPrefixGE(
prefix, key []byte, flags base.SeekGEFlags, prefix, key []byte, flags base.SeekGEFlags,
) (*base.InternalKey, base.LazyValue) { ) (*base.InternalKey, base.LazyValue) {
panic("unimplemented") p.resetKey()
p.iterKey, p.iterValue = p.iter.SeekPrefixGE(prefix, key, flags)
p.pos = pcIterPosCur
if p.iterKey == nil {
return nil, base.LazyValue{}
}
return p.findNextEntry()
} }
// SeekGE implements the InternalIterator interface. // SeekGE implements the InternalIterator interface.
@@ -145,7 +201,7 @@ func (p *pointCollapsingIterator) SeekGE(
key []byte, flags base.SeekGEFlags, key []byte, flags base.SeekGEFlags,
) (*base.InternalKey, base.LazyValue) { ) (*base.InternalKey, base.LazyValue) {
p.resetKey() p.resetKey()
p.iterKey, p.value = p.iter.SeekGE(key, flags) p.iterKey, p.iterValue = p.iter.SeekGE(key, flags)
p.pos = pcIterPosCur p.pos = pcIterPosCur
if p.iterKey == nil { if p.iterKey == nil {
return nil, base.LazyValue{} return nil, base.LazyValue{}
@@ -157,11 +213,17 @@ func (p *pointCollapsingIterator) SeekGE(
func (p *pointCollapsingIterator) SeekLT( func (p *pointCollapsingIterator) SeekLT(
key []byte, flags base.SeekLTFlags, key []byte, flags base.SeekLTFlags,
) (*base.InternalKey, base.LazyValue) { ) (*base.InternalKey, base.LazyValue) {
panic("unimplemented") p.resetKey()
p.iterKey, p.iterValue = p.iter.SeekLT(key, flags)
p.pos = pcIterPosCur
if p.iterKey == nil {
return nil, base.LazyValue{}
}
return p.findPrevEntry()
} }
func (p *pointCollapsingIterator) resetKey() { func (p *pointCollapsingIterator) resetKey() {
p.savedKey.UserKey = p.savedKey.UserKey[:0] p.savedKey.UserKey = p.savedKeyBuf[:0]
p.savedKey.Trailer = 0 p.savedKey.Trailer = 0
p.valueMerger = nil p.valueMerger = nil
p.valueBuf = p.valueBuf[:0] p.valueBuf = p.valueBuf[:0]
@@ -169,10 +231,21 @@ func (p *pointCollapsingIterator) resetKey() {
p.pos = pcIterPosCur p.pos = pcIterPosCur
} }
// findNextEntry is called to return the next key. p.iter must be positioned at func (p *pointCollapsingIterator) verifySeqNum(key *base.InternalKey) *base.InternalKey {
// the start of the first user key we are interested in. if !invariants.Enabled {
func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyValue) { return key
finishAndReturnMerge := func() (*base.InternalKey, base.LazyValue) { }
if p.fixedSeqNum == 0 || key == nil || key.Kind() == InternalKeyKindRangeDelete {
return key
}
if key.SeqNum() != p.fixedSeqNum {
panic(fmt.Sprintf("expected foreign point key to have seqnum %d, got %d", p.fixedSeqNum, key.SeqNum()))
}
return key
}
// finishAndReturnMerge finishes off the valueMerger and returns the saved key.
func (p *pointCollapsingIterator) finishAndReturnMerge() (*base.InternalKey, base.LazyValue) {
value, closer, err := p.valueMerger.Finish(true /* includesBase */) value, closer, err := p.valueMerger.Finish(true /* includesBase */)
if err != nil { if err != nil {
p.err = err p.err = err
@@ -183,45 +256,48 @@ func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyV
_ = closer.Close() _ = closer.Close()
} }
p.valueMerger = nil p.valueMerger = nil
newValue := base.MakeInPlaceValue(value) val := base.MakeInPlaceValue(p.valueBuf)
return &p.savedKey, newValue return p.verifySeqNum(&p.savedKey), val
} }
// saveKey sets p.iterKey (if not-nil) to &p.savedKey. We can use this equality // findNextEntry is called to return the next key. p.iter must be positioned at the
// as a proxy to determine if we're at the first internal key for a user key. // start of the first user key we are interested in.
func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyValue) {
p.saveKey() p.saveKey()
// Saves a comparison in the fast path
firstIteration := true
for p.iterKey != nil { for p.iterKey != nil {
// NB: p.savedKey is either the current key (iff p.iterKey == &p.savedKey), // NB: p.savedKey is either the current key (iff p.iterKey == firstKey),
// or the previous key. // or the previous key.
if p.iterKey != &p.savedKey && !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) { if !firstIteration && !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) {
if p.valueMerger != nil { if p.valueMerger != nil {
if p.savedKey.Kind() != InternalKeyKindMerge { if p.savedKey.Kind() != InternalKeyKindMerge {
panic(fmt.Sprintf("expected key %s to have MERGE kind", p.iterKey)) panic(fmt.Sprintf("expected key %s to have MERGE kind", p.iterKey))
} }
p.pos = pcIterPosNext p.pos = pcIterPosNext
return finishAndReturnMerge() return p.finishAndReturnMerge()
} }
p.saveKey() p.saveKey()
continue continue
} }
firstIteration = false
if s := p.iter.Span(); s != nil && s.CoversAt(p.seqNum, p.iterKey.SeqNum()) { if s := p.iter.Span(); s != nil && s.CoversAt(p.seqNum, p.iterKey.SeqNum()) {
// All future keys for this user key must be deleted. // All future keys for this user key must be deleted.
if p.valueMerger != nil { if p.valueMerger != nil {
return finishAndReturnMerge() return p.finishAndReturnMerge()
} else if p.savedKey.Kind() == InternalKeyKindSingleDelete { } else if p.savedKey.Kind() == InternalKeyKindSingleDelete {
panic("cannot process singledel key in point collapsing iterator") panic("cannot process singledel key in point collapsing iterator")
} }
// Fast forward to the next user key. // Fast forward to the next user key.
p.saveKey() p.saveKey()
p.iterKey, p.value = p.iter.Next() p.iterKey, p.iterValue = p.iter.Next()
for p.iterKey != nil && p.savedKey.SeqNum() >= p.iterKey.SeqNum() && p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) { for p.iterKey != nil && p.savedKey.SeqNum() >= p.iterKey.SeqNum() && p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) {
p.iterKey, p.value = p.iter.Next() p.iterKey, p.iterValue = p.iter.Next()
} }
continue continue
} }
switch p.savedKey.Kind() { switch p.savedKey.Kind() {
case InternalKeyKindSet, InternalKeyKindDelete, InternalKeyKindSetWithDelete: case InternalKeyKindSet, InternalKeyKindDelete, InternalKeyKindSetWithDelete, InternalKeyKindDeleteSized:
p.saveKey()
// Note that we return SETs directly, even if they would otherwise get // Note that we return SETs directly, even if they would otherwise get
// compacted into a Del to turn into a SetWithDelete. This is a fast // compacted into a Del to turn into a SetWithDelete. This is a fast
// path optimization that can break SINGLEDEL determinism. To lead to // path optimization that can break SINGLEDEL determinism. To lead to
@@ -240,14 +316,14 @@ func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyV
// of blocks and can determine user key changes without doing key saves // of blocks and can determine user key changes without doing key saves
// or comparisons. // or comparisons.
p.pos = pcIterPosCur p.pos = pcIterPosCur
return p.iterKey, p.value return p.verifySeqNum(p.iterKey), p.iterValue
case InternalKeyKindSingleDelete: case InternalKeyKindSingleDelete:
// Panic, as this iterator is not expected to observe single deletes. // Panic, as this iterator is not expected to observe single deletes.
panic("cannot process singledel key in point collapsing iterator") panic("cannot process singledel key in point collapsing iterator")
case InternalKeyKindMerge: case InternalKeyKindMerge:
if p.valueMerger == nil { if p.valueMerger == nil {
// Set up merger. This is the first Merge key encountered. // Set up merger. This is the first Merge key encountered.
value, callerOwned, err := p.value.Value(p.valueBuf[:0]) value, callerOwned, err := p.iterValue.Value(p.valueBuf[:0])
if err != nil { if err != nil {
p.err = err p.err = err
return nil, base.LazyValue{} return nil, base.LazyValue{}
@@ -263,13 +339,13 @@ func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyV
return nil, base.LazyValue{} return nil, base.LazyValue{}
} }
p.saveKey() p.saveKey()
p.iterKey, p.value = p.iter.Next() p.iterKey, p.iterValue = p.iter.Next()
continue continue
} }
switch p.iterKey.Kind() { switch p.iterKey.Kind() {
case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindSetWithDelete: case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindSetWithDelete:
// Merge into key. // Merge into key.
value, callerOwned, err := p.value.Value(p.valueBuf[:0]) value, callerOwned, err := p.iterValue.Value(p.valueBuf[:0])
if err != nil { if err != nil {
p.err = err p.err = err
return nil, base.LazyValue{} return nil, base.LazyValue{}
@@ -287,31 +363,144 @@ func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyV
if p.iterKey.Kind() != InternalKeyKindMerge { if p.iterKey.Kind() != InternalKeyKindMerge {
p.savedKey.SetKind(p.iterKey.Kind()) p.savedKey.SetKind(p.iterKey.Kind())
p.pos = pcIterPosCur p.pos = pcIterPosCur
return finishAndReturnMerge() return p.finishAndReturnMerge()
} }
p.iterKey, p.value = p.iter.Next() p.iterKey, p.iterValue = p.iter.Next()
case InternalKeyKindRangeDelete: case InternalKeyKindRangeDelete:
if p.elideRangeDeletes {
// Skip this range delete, and process any point after it.
p.iterKey, p.iterValue = p.iter.Next()
p.saveKey()
continue
}
// These are interleaved by the interleaving iterator ahead of all points. // These are interleaved by the interleaving iterator ahead of all points.
// We should pass them as-is, but also account for any points ahead of // We should pass them as-is, but also account for any points ahead of
// them. // them.
p.pos = pcIterPosCur p.pos = pcIterPosCur
return p.iterKey, p.value return p.verifySeqNum(p.iterKey), p.iterValue
default: default:
panic(fmt.Sprintf("unexpected kind: %d", p.iterKey.Kind())) panic(fmt.Sprintf("unexpected kind: %d", p.iterKey.Kind()))
} }
} }
if p.valueMerger != nil { if p.valueMerger != nil {
p.pos = pcIterPosNext p.pos = pcIterPosNext
return finishAndReturnMerge() return p.finishAndReturnMerge()
} }
p.resetKey() p.resetKey()
return nil, base.LazyValue{} return nil, base.LazyValue{}
} }
// findPrevEntry finds the relevant point key to return for the previous user key
// (i.e. in reverse iteration). Requires that the iterator is already positioned
// at the first-in-reverse (i.e. rightmost / largest) internal key encountered
// for that user key.
func (p *pointCollapsingIterator) findPrevEntry() (*base.InternalKey, base.LazyValue) {
if p.iterKey == nil {
p.pos = pcIterPosCur
return nil, base.LazyValue{}
}
p.saveKey()
firstIteration := true
for p.iterKey != nil {
if !firstIteration && !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) {
p.pos = pcIterPosPrev
return p.verifySeqNum(&p.savedKey), p.savedValue
}
firstIteration = false
if s := p.iter.Span(); s != nil && s.CoversAt(p.seqNum, p.iterKey.SeqNum()) {
// Skip this key.
p.iterKey, p.iterValue = p.iter.Prev()
p.saveKey()
continue
}
switch p.iterKey.Kind() {
case InternalKeyKindSet, InternalKeyKindDelete, InternalKeyKindSetWithDelete:
// Instead of calling saveKey(), we take advantage of the invariant that
// p.savedKey.UserKey == p.iterKey.UserKey (otherwise we'd have gone into
// the !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) case at
// the top of this for loop). That allows us to just save the trailer
// and move on.
if invariants.Enabled && !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) {
panic("unexpected inequality between p.iterKey and p.savedKey")
}
p.savedKey.Trailer = p.iterKey.Trailer
// Copy value into p.savedValue.
value, callerOwned, err := p.iterValue.Value(p.valueBuf[:0])
if err != nil {
p.err = err
return nil, base.LazyValue{}
}
if !callerOwned {
p.valueBuf = append(p.valueBuf[:0], value...)
} else {
p.valueBuf = value
}
p.valueMerger = nil
p.savedValue = base.MakeInPlaceValue(p.valueBuf)
p.iterKey, p.iterValue = p.iter.Prev()
continue
case InternalKeyKindSingleDelete:
// Panic, as this iterator is not expected to observe single deletes.
panic("cannot process singledel key in point collapsing iterator")
case InternalKeyKindMerge:
panic("cannot process merge key in point collapsing iterator in reverse iteration")
case InternalKeyKindRangeDelete:
if p.elideRangeDeletes {
// Skip this range delete, and process any point before it.
p.iterKey, p.iterValue = p.iter.Prev()
continue
}
// These are interleaved by the interleaving iterator behind all points.
if p.savedKey.Kind() != InternalKeyKindRangeDelete {
// If the previous key was not a rangedel, we need to return it. Pretend that we're at the
// previous user key (i.e. with p.pos = pcIterPosPrev) even if we're not, so on the next
// Prev() we encounter and return this rangedel. For now return the point ahead of
// this range del (if any).
p.pos = pcIterPosPrev
return p.verifySeqNum(&p.savedKey), p.savedValue
}
// We take advantage of the fact that a Prev() *on* a RangeDel iterKey
// always takes us to a different user key, so on the next iteration
// we will fall into the !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey)
// case.
//
// Instead of calling saveKey(), we take advantage of the invariant that
// p.savedKey.UserKey == p.iterKey.UserKey (otherwise we'd have gone into
// the !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) case at
// the top of this for loop). That allows us to just save the trailer
// and move on.
if invariants.Enabled && !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) {
panic("unexpected inequality between p.iterKey and p.savedKey")
}
p.savedKey.Trailer = p.iterKey.Trailer
// Copy value into p.savedValue.
value, callerOwned, err := p.iterValue.Value(p.valueBuf[:0])
if err != nil {
p.err = err
return nil, base.LazyValue{}
}
if !callerOwned {
p.valueBuf = append(p.valueBuf[:0], value...)
} else {
p.valueBuf = value
}
p.valueMerger = nil
p.savedValue = base.MakeInPlaceValue(p.valueBuf)
p.iterKey, p.iterValue = p.iter.Prev()
continue
default:
panic(fmt.Sprintf("unexpected kind: %d", p.iterKey.Kind()))
}
}
p.pos = pcIterPosPrev
return p.verifySeqNum(&p.savedKey), p.savedValue
}
// First implements the InternalIterator interface. // First implements the InternalIterator interface.
func (p *pointCollapsingIterator) First() (*base.InternalKey, base.LazyValue) { func (p *pointCollapsingIterator) First() (*base.InternalKey, base.LazyValue) {
p.resetKey() p.resetKey()
p.iterKey, p.value = p.iter.First() p.iterKey, p.iterValue = p.iter.First()
p.pos = pcIterPosCur p.pos = pcIterPosCur
if p.iterKey == nil { if p.iterKey == nil {
return nil, base.LazyValue{} return nil, base.LazyValue{}
@@ -321,41 +510,40 @@ func (p *pointCollapsingIterator) First() (*base.InternalKey, base.LazyValue) {
// Last implements the InternalIterator interface. // Last implements the InternalIterator interface.
func (p *pointCollapsingIterator) Last() (*base.InternalKey, base.LazyValue) { func (p *pointCollapsingIterator) Last() (*base.InternalKey, base.LazyValue) {
panic("unimplemented") p.resetKey()
p.iterKey, p.iterValue = p.iter.Last()
p.pos = pcIterPosCur
if p.iterKey == nil {
return nil, base.LazyValue{}
}
return p.findPrevEntry()
} }
func (p *pointCollapsingIterator) saveKey() { func (p *pointCollapsingIterator) saveKey() {
if p.iterKey == nil { if p.iterKey == nil {
p.savedKey = InternalKey{UserKey: p.savedKey.UserKey[:0]} p.savedKey = InternalKey{UserKey: p.savedKeyBuf[:0]}
return
} else if p.iterKey == &p.savedKey {
// Do nothing.
return return
} }
p.savedKey.CopyFrom(*p.iterKey) p.savedKeyBuf = append(p.savedKeyBuf[:0], p.iterKey.UserKey...)
p.iterKey = &p.savedKey p.savedKey = InternalKey{UserKey: p.savedKeyBuf, Trailer: p.iterKey.Trailer}
} }
// Next implements the InternalIterator interface. // Next implements the InternalIterator interface.
func (p *pointCollapsingIterator) Next() (*base.InternalKey, base.LazyValue) { func (p *pointCollapsingIterator) Next() (*base.InternalKey, base.LazyValue) {
switch p.pos { switch p.pos {
case pcIterPosCur: case pcIterPosPrev:
p.saveKey() p.saveKey()
if p.iterKey != nil && p.iterKey.Kind() == InternalKeyKindRangeDelete { if p.iterKey != nil && p.iterKey.Kind() == InternalKeyKindRangeDelete && !p.elideRangeDeletes {
// Step over the interleaved range delete and process the very next p.iterKey, p.iterValue = p.iter.Next()
// internal key, even if it's at the same user key. This is because a p.pos = pcIterPosCur
// point for that user key has not been returned yet. } else {
p.iterKey, p.value = p.iter.Next() // Fast forward to the next user key.
break
}
// Fast forward to the next user key. p.iterKey stayed at p.savedKey for
// this loop.
key, val := p.iter.Next() key, val := p.iter.Next()
// p.iterKey.SeqNum() >= key.SeqNum() is an optimization that allows us to // p.iterKey.SeqNum() >= key.SeqNum() is an optimization that allows us to
// use p.iterKey.SeqNum() < key.SeqNum() as a sign that the user key has // use p.iterKey.SeqNum() < key.SeqNum() as a sign that the user key has
// changed, without needing to do the full key comparison. // changed, without needing to do the full key comparison.
for p.iterKey != nil && key != nil && p.iterKey.SeqNum() >= key.SeqNum() && for key != nil && p.savedKey.SeqNum() >= key.SeqNum() &&
p.comparer.Equal(p.iterKey.UserKey, key.UserKey) { p.comparer.Equal(p.savedKey.UserKey, key.UserKey) {
key, val = p.iter.Next() key, val = p.iter.Next()
} }
if key == nil { if key == nil {
@@ -363,7 +551,34 @@ func (p *pointCollapsingIterator) Next() (*base.InternalKey, base.LazyValue) {
p.resetKey() p.resetKey()
return nil, base.LazyValue{} return nil, base.LazyValue{}
} }
p.iterKey, p.value = key, val p.iterKey, p.iterValue = key, val
p.pos = pcIterPosCur
}
fallthrough
case pcIterPosCur:
p.saveKey()
if p.iterKey != nil && p.iterKey.Kind() == InternalKeyKindRangeDelete && !p.elideRangeDeletes {
// Step over the interleaved range delete and process the very next
// internal key, even if it's at the same user key. This is because a
// point for that user key has not been returned yet.
p.iterKey, p.iterValue = p.iter.Next()
break
}
// Fast forward to the next user key.
key, val := p.iter.Next()
// p.iterKey.SeqNum() >= key.SeqNum() is an optimization that allows us to
// use p.iterKey.SeqNum() < key.SeqNum() as a sign that the user key has
// changed, without needing to do the full key comparison.
for key != nil && p.savedKey.SeqNum() >= key.SeqNum() &&
p.comparer.Equal(p.savedKey.UserKey, key.UserKey) {
key, val = p.iter.Next()
}
if key == nil {
// There are no keys to return.
p.resetKey()
return nil, base.LazyValue{}
}
p.iterKey, p.iterValue = key, val
case pcIterPosNext: case pcIterPosNext:
p.pos = pcIterPosCur p.pos = pcIterPosCur
} }
@@ -376,12 +591,56 @@ func (p *pointCollapsingIterator) Next() (*base.InternalKey, base.LazyValue) {
// NextPrefix implements the InternalIterator interface. // NextPrefix implements the InternalIterator interface.
func (p *pointCollapsingIterator) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { func (p *pointCollapsingIterator) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) {
// TODO(bilal): Implement this. It'll be similar to SeekGE, except we'll call
// the child iterator's NextPrefix, and have some special logic in case pos
// is pcIterPosNext.
panic("unimplemented") panic("unimplemented")
} }
// Prev implements the InternalIterator interface. // Prev implements the InternalIterator interface.
func (p *pointCollapsingIterator) Prev() (*base.InternalKey, base.LazyValue) { func (p *pointCollapsingIterator) Prev() (*base.InternalKey, base.LazyValue) {
panic("unimplemented") switch p.pos {
case pcIterPosNext:
// Rewind backwards to the previous iter key.
p.saveKey()
key, val := p.iter.Prev()
for key != nil && p.savedKey.SeqNum() <= key.SeqNum() &&
p.comparer.Equal(p.savedKey.UserKey, key.UserKey) {
if key.Kind() == InternalKeyKindRangeDelete && !p.elideRangeDeletes {
// We need to pause at this range delete and return it as-is, as "cur"
// is referencing the point key after it, not the range delete.
break
}
key, val = p.iter.Prev()
}
p.iterKey = key
p.iterValue = val
p.pos = pcIterPosCur
fallthrough
case pcIterPosCur:
p.saveKey()
key, val := p.iter.Prev()
for key != nil && p.savedKey.SeqNum() <= key.SeqNum() &&
p.comparer.Equal(p.savedKey.UserKey, key.UserKey) {
if key.Kind() == InternalKeyKindRangeDelete && !p.elideRangeDeletes {
// We need to pause at this range delete and return it as-is, as "cur"
// is referencing the point key after it, not the range delete.
break
}
key, val = p.iter.Prev()
}
p.iterKey = key
p.iterValue = val
p.pos = pcIterPosCur
case pcIterPosPrev:
// Do nothing.
p.pos = pcIterPosCur
}
if p.iterKey == nil {
p.resetKey()
return nil, base.LazyValue{}
}
return p.findPrevEntry()
} }
// Error implements the InternalIterator interface. // Error implements the InternalIterator interface.
@@ -477,10 +736,6 @@ func (d *DB) truncateSharedFile(
// We will need to truncate file bounds in at least one direction. Open all // We will need to truncate file bounds in at least one direction. Open all
// relevant iterators. // relevant iterators.
//
// TODO(bilal): Once virtual sstables go in, verify that the constraining of
// bounds to virtual sstable bounds happens below this method, so we aren't
// unintentionally exposing keys we shouldn't be exposing.
iter, rangeDelIter, err := d.newIters(ctx, file, &IterOptions{ iter, rangeDelIter, err := d.newIters(ctx, file, &IterOptions{
LowerBound: lower, LowerBound: lower,
UpperBound: upper, UpperBound: upper,
@@ -497,7 +752,7 @@ func (d *DB) truncateSharedFile(
) )
defer rangeDelIter.Close() defer rangeDelIter.Close()
} }
rangeKeyIter, err := d.tableNewRangeKeyIter(file, nil /* spanIterOptions */) rangeKeyIter, err := d.tableNewRangeKeyIter(file, keyspan.SpanIterOptions{Level: manifest.Level(level)})
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@@ -584,6 +839,10 @@ func (d *DB) truncateSharedFile(
if len(sst.Smallest.UserKey) == 0 { if len(sst.Smallest.UserKey) == 0 {
return nil, true, nil return nil, true, nil
} }
sst.Size, err = d.tableCache.estimateSize(file, sst.Smallest.UserKey, sst.Largest.UserKey)
if err != nil {
return nil, false, err
}
return sst, false, nil return sst, false, nil
} }
@@ -616,7 +875,7 @@ func scanInternalImpl(
for f := files.SeekGE(cmp, lower); f != nil && cmp(f.Smallest.UserKey, upper) < 0; f = files.Next() { for f := files.SeekGE(cmp, lower); f != nil && cmp(f.Smallest.UserKey, upper) < 0; f = files.Next() {
var objMeta objstorage.ObjectMetadata var objMeta objstorage.ObjectMetadata
var err error var err error
objMeta, err = provider.Lookup(fileTypeTable, f.FileNum.DiskFileNum()) objMeta, err = provider.Lookup(fileTypeTable, f.FileBacking.DiskFileNum)
if err != nil { if err != nil {
return err return err
} }
@@ -718,6 +977,7 @@ func (i *scanInternalIterator) constructPointIter(memtables flushableList, buf *
mlevels = mlevels[:numMergingLevels] mlevels = mlevels[:numMergingLevels]
levels = levels[:numLevelIters] levels = levels[:numLevelIters]
rangeDelLevels = rangeDelLevels[:numLevelIters] rangeDelLevels = rangeDelLevels[:numLevelIters]
i.opts.IterOptions.snapshotForHideObsoletePoints = i.seqNum
addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) { addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) {
li := &levels[levelsIndex] li := &levels[levelsIndex]
rli := &rangeDelLevels[levelsIndex] rli := &rangeDelLevels[levelsIndex]
@@ -804,8 +1064,7 @@ func (i *scanInternalIterator) constructRangeKeyIter() {
// around Key Trailer order. // around Key Trailer order.
iter := current.RangeKeyLevels[0].Iter() iter := current.RangeKeyLevels[0].Iter()
for f := iter.Last(); f != nil; f = iter.Prev() { for f := iter.Last(); f != nil; f = iter.Prev() {
spanIterOpts := &keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} spanIter, err := i.newIterRangeKey(f, i.opts.SpanIterOptions(manifest.Level(0)))
spanIter, err := i.newIterRangeKey(f, spanIterOpts)
if err != nil { if err != nil {
i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err}) i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err})
continue continue
@@ -822,7 +1081,7 @@ func (i *scanInternalIterator) constructRangeKeyIter() {
continue continue
} }
li := i.rangeKey.iterConfig.NewLevelIter() li := i.rangeKey.iterConfig.NewLevelIter()
spanIterOpts := keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} spanIterOpts := i.opts.SpanIterOptions(manifest.Level(level))
li.Init(spanIterOpts, i.comparer.Compare, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(), li.Init(spanIterOpts, i.comparer.Compare, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(),
manifest.Level(level), manifest.KeyTypeRange) manifest.Level(level), manifest.KeyTypeRange)
i.rangeKey.iterConfig.AddLevel(li) i.rangeKey.iterConfig.AddLevel(li)

View File

@@ -47,6 +47,9 @@ type blockWriter struct {
// ensuring that any of the keys with the same prefix can be used to // ensuring that any of the keys with the same prefix can be used to
// assemble the full key when the prefix does change. // assemble the full key when the prefix does change.
restarts []uint32 restarts []uint32
// Do not read curKey directly from outside blockWriter since it can have
// the InternalKeyKindSSTableInternalObsoleteBit set. Use getCurKey() or
// getCurUserKey() instead.
curKey []byte curKey []byte
// curValue excludes the optional prefix provided to // curValue excludes the optional prefix provided to
// storeWithOptionalValuePrefix. // storeWithOptionalValuePrefix.
@@ -79,6 +82,20 @@ const setHasSameKeyPrefixRestartMask uint32 = 1 << 31
const restartMaskLittleEndianHighByteWithoutSetHasSamePrefix byte = 0b0111_1111 const restartMaskLittleEndianHighByteWithoutSetHasSamePrefix byte = 0b0111_1111
const restartMaskLittleEndianHighByteOnlySetHasSamePrefix byte = 0b1000_0000 const restartMaskLittleEndianHighByteOnlySetHasSamePrefix byte = 0b1000_0000
func (w *blockWriter) getCurKey() InternalKey {
k := base.DecodeInternalKey(w.curKey)
k.Trailer = k.Trailer & trailerObsoleteMask
return k
}
func (w *blockWriter) getCurUserKey() []byte {
n := len(w.curKey) - base.InternalTrailerLen
if n < 0 {
panic(errors.AssertionFailedf("corrupt key in blockWriter buffer"))
}
return w.curKey[:n:n]
}
// If !addValuePrefix, the valuePrefix is ignored. // If !addValuePrefix, the valuePrefix is ignored.
func (w *blockWriter) storeWithOptionalValuePrefix( func (w *blockWriter) storeWithOptionalValuePrefix(
keySize int, keySize int,
@@ -191,12 +208,21 @@ func (w *blockWriter) storeWithOptionalValuePrefix(
func (w *blockWriter) add(key InternalKey, value []byte) { func (w *blockWriter) add(key InternalKey, value []byte) {
w.addWithOptionalValuePrefix( w.addWithOptionalValuePrefix(
key, value, len(key.UserKey), false, 0, false) key, false, value, len(key.UserKey), false, 0, false)
} }
// Callers that always set addValuePrefix to false should use add() instead. // Callers that always set addValuePrefix to false should use add() instead.
//
// isObsolete indicates whether this key-value pair is obsolete in this
// sstable (only applicable when writing data blocks) -- see the comment in
// table.go and the longer one in format.go. addValuePrefix adds a 1 byte
// prefix to the value, specified in valuePrefix -- this is used for data
// blocks in TableFormatPebblev3 onwards for SETs (see the comment in
// format.go, with more details in value_block.go). setHasSameKeyPrefix is
// also used in TableFormatPebblev3 onwards for SETs.
func (w *blockWriter) addWithOptionalValuePrefix( func (w *blockWriter) addWithOptionalValuePrefix(
key InternalKey, key InternalKey,
isObsolete bool,
value []byte, value []byte,
maxSharedKeyLen int, maxSharedKeyLen int,
addValuePrefix bool, addValuePrefix bool,
@@ -210,6 +236,9 @@ func (w *blockWriter) addWithOptionalValuePrefix(
w.curKey = make([]byte, 0, size*2) w.curKey = make([]byte, 0, size*2)
} }
w.curKey = w.curKey[:size] w.curKey = w.curKey[:size]
if isObsolete {
key.Trailer = key.Trailer | trailerObsoleteBit
}
key.Encode(w.curKey) key.Encode(w.curKey)
w.storeWithOptionalValuePrefix( w.storeWithOptionalValuePrefix(
@@ -374,13 +403,14 @@ type blockIter struct {
cached []blockEntry cached []blockEntry
cachedBuf []byte cachedBuf []byte
cacheHandle cache.Handle cacheHandle cache.Handle
// The first key in the block. This is used by the caller to set bounds // The first user key in the block. This is used by the caller to set bounds
// for block iteration for already loaded blocks. // for block iteration for already loaded blocks.
firstKey InternalKey firstUserKey []byte
lazyValueHandling struct { lazyValueHandling struct {
vbr *valueBlockReader vbr *valueBlockReader
hasValuePrefix bool hasValuePrefix bool
} }
hideObsoletePoints bool
} }
// blockIter implements the base.InternalIterator interface. // blockIter implements the base.InternalIterator interface.
@@ -388,14 +418,16 @@ var _ base.InternalIterator = (*blockIter)(nil)
func newBlockIter(cmp Compare, block block) (*blockIter, error) { func newBlockIter(cmp Compare, block block) (*blockIter, error) {
i := &blockIter{} i := &blockIter{}
return i, i.init(cmp, block, 0) return i, i.init(cmp, block, 0, false)
} }
func (i *blockIter) String() string { func (i *blockIter) String() string {
return "block" return "block"
} }
func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error { func (i *blockIter) init(
cmp Compare, block block, globalSeqNum uint64, hideObsoletePoints bool,
) error {
numRestarts := int32(binary.LittleEndian.Uint32(block[len(block)-4:])) numRestarts := int32(binary.LittleEndian.Uint32(block[len(block)-4:]))
if numRestarts == 0 { if numRestarts == 0 {
return base.CorruptionErrorf("pebble/table: invalid table (block has no restart points)") return base.CorruptionErrorf("pebble/table: invalid table (block has no restart points)")
@@ -408,6 +440,7 @@ func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error {
i.data = block i.data = block
i.fullKey = i.fullKey[:0] i.fullKey = i.fullKey[:0]
i.val = nil i.val = nil
i.hideObsoletePoints = hideObsoletePoints
i.clearCache() i.clearCache()
if i.restarts > 0 { if i.restarts > 0 {
if err := i.readFirstKey(); err != nil { if err := i.readFirstKey(); err != nil {
@@ -415,15 +448,21 @@ func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error {
} }
} else { } else {
// Block is empty. // Block is empty.
i.firstKey = InternalKey{} i.firstUserKey = nil
} }
return nil return nil
} }
func (i *blockIter) initHandle(cmp Compare, block cache.Handle, globalSeqNum uint64) error { // NB: two cases of hideObsoletePoints:
// - Local sstable iteration: globalSeqNum will be set iff the sstable was
// ingested.
// - Foreign sstable iteration: globalSeqNum is always set.
func (i *blockIter) initHandle(
cmp Compare, block cache.Handle, globalSeqNum uint64, hideObsoletePoints bool,
) error {
i.cacheHandle.Release() i.cacheHandle.Release()
i.cacheHandle = block i.cacheHandle = block
return i.init(cmp, block.Get(), globalSeqNum) return i.init(cmp, block.Get(), globalSeqNum, hideObsoletePoints)
} }
func (i *blockIter) invalidate() { func (i *blockIter) invalidate() {
@@ -590,24 +629,27 @@ func (i *blockIter) readFirstKey() error {
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on
// BlockIter benchmarks. // BlockIter benchmarks.
if n := len(firstKey) - 8; n >= 0 { if n := len(firstKey) - 8; n >= 0 {
i.firstKey.Trailer = binary.LittleEndian.Uint64(firstKey[n:]) i.firstUserKey = firstKey[:n:n]
i.firstKey.UserKey = firstKey[:n:n]
if i.globalSeqNum != 0 {
i.firstKey.SetSeqNum(i.globalSeqNum)
}
} else { } else {
i.firstKey.Trailer = uint64(InternalKeyKindInvalid) i.firstUserKey = nil
i.firstKey.UserKey = nil
return base.CorruptionErrorf("pebble/table: invalid firstKey in block") return base.CorruptionErrorf("pebble/table: invalid firstKey in block")
} }
return nil return nil
} }
func (i *blockIter) decodeInternalKey(key []byte) { // The sstable internal obsolete bit is set when writing a block and unset by
// blockIter, so no code outside block writing/reading code ever sees it.
const trailerObsoleteBit = uint64(base.InternalKeyKindSSTableInternalObsoleteBit)
const trailerObsoleteMask = (InternalKeySeqNumMax << 8) | uint64(base.InternalKeyKindSSTableInternalObsoleteMask)
func (i *blockIter) decodeInternalKey(key []byte) (hiddenPoint bool) {
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on
// BlockIter benchmarks. // BlockIter benchmarks.
if n := len(key) - 8; n >= 0 { if n := len(key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(key[n:]) trailer := binary.LittleEndian.Uint64(key[n:])
hiddenPoint = i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = key[:n:n] i.ikey.UserKey = key[:n:n]
if i.globalSeqNum != 0 { if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum) i.ikey.SetSeqNum(i.globalSeqNum)
@@ -616,6 +658,7 @@ func (i *blockIter) decodeInternalKey(key []byte) {
i.ikey.Trailer = uint64(InternalKeyKindInvalid) i.ikey.Trailer = uint64(InternalKeyKindInvalid)
i.ikey.UserKey = nil i.ikey.UserKey = nil
} }
return hiddenPoint
} }
func (i *blockIter) clearCache() { func (i *blockIter) clearCache() {
@@ -640,13 +683,15 @@ func (i *blockIter) cacheEntry() {
i.cachedBuf = append(i.cachedBuf, i.key...) i.cachedBuf = append(i.cachedBuf, i.key...)
} }
func (i *blockIter) getFirstUserKey() []byte {
return i.firstUserKey
}
// SeekGE implements internalIterator.SeekGE, as documented in the pebble // SeekGE implements internalIterator.SeekGE, as documented in the pebble
// package. // package.
func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) { func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) {
i.clearCache() i.clearCache()
ikey := base.MakeSearchKey(key)
// Find the index of the smallest restart point whose key is > the key // Find the index of the smallest restart point whose key is > the key
// sought; index will be numRestarts if there is no such restart point. // sought; index will be numRestarts if there is no such restart point.
i.offset = 0 i.offset = 0
@@ -700,23 +745,23 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
ptr = unsafe.Pointer(uintptr(ptr) + 5) ptr = unsafe.Pointer(uintptr(ptr) + 5)
} }
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on // Manually inlining part of base.DecodeInternalKey provides a 5-10%
// BlockIter benchmarks. // speedup on BlockIter benchmarks.
s := getBytes(ptr, int(v1)) s := getBytes(ptr, int(v1))
var k InternalKey var k []byte
if n := len(s) - 8; n >= 0 { if n := len(s) - 8; n >= 0 {
k.Trailer = binary.LittleEndian.Uint64(s[n:]) k = s[:n:n]
k.UserKey = s[:n:n]
// NB: We can't have duplicate keys if the globalSeqNum != 0, so we
// leave the seqnum on this key as 0 as it won't affect our search
// since ikey has the maximum seqnum.
} else {
k.Trailer = uint64(InternalKeyKindInvalid)
} }
// Else k is invalid, and left as nil
if base.InternalCompare(i.cmp, ikey, k) >= 0 { if i.cmp(key, k) > 0 {
// The search key is greater than the user key at this restart point.
// Search beyond this restart point, since we are trying to find the
// first restart point with a user key >= the search key.
index = h + 1 // preserves f(i-1) == false index = h + 1 // preserves f(i-1) == false
} else { } else {
// k >= search key, so prune everything after index (since index
// satisfies the property we are looking for).
upper = h // preserves f(j) == true upper = h // preserves f(j) == true
} }
} }
@@ -724,21 +769,25 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
// => answer is index. // => answer is index.
} }
// Since keys are strictly increasing, if index > 0 then the restart point at // index is the first restart point with key >= search key. Define the keys
// index-1 will be the largest whose key is <= the key sought. If index == // between a restart point and the next restart point as belonging to that
// 0, then all keys in this block are larger than the key sought, and offset // restart point.
// remains at zero. //
// Since keys are strictly increasing, if index > 0 then the restart point
// at index-1 will be the first one that has some keys belonging to it that
// could be equal to the search key. If index == 0, then all keys in this
// block are larger than the key sought, and offset remains at zero.
if index > 0 { if index > 0 {
i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) i.offset = decodeRestart(i.data[i.restarts+4*(index-1):])
} }
i.readEntry() i.readEntry()
i.decodeInternalKey(i.key) hiddenPoint := i.decodeInternalKey(i.key)
// Iterate from that restart point to somewhere >= the key sought. // Iterate from that restart point to somewhere >= the key sought.
if !i.valid() { if !i.valid() {
return nil, base.LazyValue{} return nil, base.LazyValue{}
} }
if base.InternalCompare(i.cmp, i.ikey, ikey) >= 0 { if !hiddenPoint && i.cmp(i.ikey.UserKey, key) >= 0 {
// Initialize i.lazyValue // Initialize i.lazyValue
if !i.lazyValueHandling.hasValuePrefix || if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
@@ -751,7 +800,7 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba
return &i.ikey, i.lazyValue return &i.ikey, i.lazyValue
} }
for i.Next(); i.valid(); i.Next() { for i.Next(); i.valid(); i.Next() {
if base.InternalCompare(i.cmp, i.ikey, ikey) >= 0 { if i.cmp(i.ikey.UserKey, key) >= 0 {
// i.Next() has already initialized i.lazyValue. // i.Next() has already initialized i.lazyValue.
return &i.ikey, i.lazyValue return &i.ikey, i.lazyValue
} }
@@ -773,8 +822,6 @@ func (i *blockIter) SeekPrefixGE(
func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) { func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) {
i.clearCache() i.clearCache()
ikey := base.MakeSearchKey(key)
// Find the index of the smallest restart point whose key is >= the key // Find the index of the smallest restart point whose key is >= the key
// sought; index will be numRestarts if there is no such restart point. // sought; index will be numRestarts if there is no such restart point.
i.offset = 0 i.offset = 0
@@ -828,23 +875,23 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
ptr = unsafe.Pointer(uintptr(ptr) + 5) ptr = unsafe.Pointer(uintptr(ptr) + 5)
} }
// Manually inlining base.DecodeInternalKey provides a 5-10% speedup on // Manually inlining part of base.DecodeInternalKey provides a 5-10%
// BlockIter benchmarks. // speedup on BlockIter benchmarks.
s := getBytes(ptr, int(v1)) s := getBytes(ptr, int(v1))
var k InternalKey var k []byte
if n := len(s) - 8; n >= 0 { if n := len(s) - 8; n >= 0 {
k.Trailer = binary.LittleEndian.Uint64(s[n:]) k = s[:n:n]
k.UserKey = s[:n:n]
// NB: We can't have duplicate keys if the globalSeqNum != 0, so we
// leave the seqnum on this key as 0 as it won't affect our search
// since ikey has the maximum seqnum.
} else {
k.Trailer = uint64(InternalKeyKindInvalid)
} }
// Else k is invalid, and left as nil
if base.InternalCompare(i.cmp, ikey, k) > 0 { if i.cmp(key, k) > 0 {
// The search key is greater than the user key at this restart point.
// Search beyond this restart point, since we are trying to find the
// first restart point with a user key >= the search key.
index = h + 1 // preserves f(i-1) == false index = h + 1 // preserves f(i-1) == false
} else { } else {
// k >= search key, so prune everything after index (since index
// satisfies the property we are looking for).
upper = h // preserves f(j) == true upper = h // preserves f(j) == true
} }
} }
@@ -852,8 +899,15 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
// => answer is index. // => answer is index.
} }
// Since keys are strictly increasing, if index > 0 then the restart point at // index is the first restart point with key >= search key. Define the keys
// index-1 will be the largest whose key is < the key sought. // between a restart point and the next restart point as belonging to that
// restart point. Note that index could be equal to i.numRestarts, i.e., we
// are past the last restart.
//
// Since keys are strictly increasing, if index > 0 then the restart point
// at index-1 will be the first one that has some keys belonging to it that
// are less than the search key. If index == 0, then all keys in this block
// are larger than the search key, so there is no match.
targetOffset := i.restarts targetOffset := i.restarts
if index > 0 { if index > 0 {
i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) i.offset = decodeRestart(i.data[i.restarts+4*(index-1):])
@@ -876,24 +930,41 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba
for { for {
i.offset = i.nextOffset i.offset = i.nextOffset
i.readEntry() i.readEntry()
i.decodeInternalKey(i.key) // When hidden keys are common, there is additional optimization possible
// by not caching entries that are hidden (note that some calls to
// cacheEntry don't decode the internal key before caching, but checking
// whether a key is hidden does not require full decoding). However, we do
// need to use the blockEntry.offset in the cache for the first entry at
// the reset point to do the binary search when the cache is empty -- so
// we would need to cache that first entry (though not the key) even if
// was hidden. Our current assumption is that if there are large numbers
// of hidden keys we will be able to skip whole blocks (using block
// property filters) so we don't bother optimizing.
hiddenPoint := i.decodeInternalKey(i.key)
if i.cmp(i.ikey.UserKey, ikey.UserKey) >= 0 { // NB: we don't use the hiddenPoint return value of decodeInternalKey
// since we want to stop as soon as we reach a key >= ikey.UserKey, so
// that we can reverse.
if i.cmp(i.ikey.UserKey, key) >= 0 {
// The current key is greater than or equal to our search key. Back up to // The current key is greater than or equal to our search key. Back up to
// the previous key which was less than our search key. Note that this for // the previous key which was less than our search key. Note that this for
// loop will execute at least once with this if-block not being true, so // loop will execute at least once with this if-block not being true, so
// the key we are backing up to is the last one this loop cached. // the key we are backing up to is the last one this loop cached.
i.Prev() return i.Prev()
// i.Prev() has already initialized i.lazyValue.
return &i.ikey, i.lazyValue
} }
if i.nextOffset >= targetOffset { if i.nextOffset >= targetOffset {
// We've reached the end of the current restart block. Return the current // We've reached the end of the current restart block. Return the
// key. When the restart interval is 1, the first iteration of the for // current key if not hidden, else call Prev().
// loop will bring us here. In that case ikey is backed by the block so //
// we get the desired key stability guarantee for the lifetime of the // When the restart interval is 1, the first iteration of the for loop
// blockIter. // will bring us here. In that case ikey is backed by the block so we
// get the desired key stability guarantee for the lifetime of the
// blockIter. That is, we never cache anything and therefore never
// return a key backed by cachedBuf.
if hiddenPoint {
return i.Prev()
}
break break
} }
@@ -923,7 +994,10 @@ func (i *blockIter) First() (*InternalKey, base.LazyValue) {
} }
i.clearCache() i.clearCache()
i.readEntry() i.readEntry()
i.decodeInternalKey(i.key) hiddenPoint := i.decodeInternalKey(i.key)
if hiddenPoint {
return i.Next()
}
if !i.lazyValueHandling.hasValuePrefix || if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
i.lazyValue = base.MakeInPlaceValue(i.val) i.lazyValue = base.MakeInPlaceValue(i.val)
@@ -958,7 +1032,10 @@ func (i *blockIter) Last() (*InternalKey, base.LazyValue) {
i.readEntry() i.readEntry()
} }
i.decodeInternalKey(i.key) hiddenPoint := i.decodeInternalKey(i.key)
if hiddenPoint {
return i.Prev()
}
if !i.lazyValueHandling.hasValuePrefix || if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
i.lazyValue = base.MakeInPlaceValue(i.val) i.lazyValue = base.MakeInPlaceValue(i.val)
@@ -988,6 +1065,7 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) {
i.clearCache() i.clearCache()
} }
start:
i.offset = i.nextOffset i.offset = i.nextOffset
if !i.valid() { if !i.valid() {
return nil, base.LazyValue{} return nil, base.LazyValue{}
@@ -995,11 +1073,17 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) {
i.readEntry() i.readEntry()
// Manually inlined version of i.decodeInternalKey(i.key). // Manually inlined version of i.decodeInternalKey(i.key).
if n := len(i.key) - 8; n >= 0 { if n := len(i.key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:]) trailer := binary.LittleEndian.Uint64(i.key[n:])
hiddenPoint := i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = i.key[:n:n] i.ikey.UserKey = i.key[:n:n]
if i.globalSeqNum != 0 { if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum) i.ikey.SetSeqNum(i.globalSeqNum)
} }
if hiddenPoint {
goto start
}
} else { } else {
i.ikey.Trailer = uint64(InternalKeyKindInvalid) i.ikey.Trailer = uint64(InternalKeyKindInvalid)
i.ikey.UserKey = nil i.ikey.UserKey = nil
@@ -1015,8 +1099,8 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) {
return &i.ikey, i.lazyValue return &i.ikey, i.lazyValue
} }
// nextPrefix is used for implementing NPrefix. // NextPrefix implements (base.InternalIterator).NextPrefix.
func (i *blockIter) nextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) {
if i.lazyValueHandling.hasValuePrefix { if i.lazyValueHandling.hasValuePrefix {
return i.nextPrefixV3(succKey) return i.nextPrefixV3(succKey)
} }
@@ -1141,6 +1225,7 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
// The trailer is written in little endian, so the key kind is the first // The trailer is written in little endian, so the key kind is the first
// byte in the trailer that is encoded in the slice [unshared-8:unshared]. // byte in the trailer that is encoded in the slice [unshared-8:unshared].
keyKind := InternalKeyKind((*[manual.MaxArrayLen]byte)(ptr)[unshared-8]) keyKind := InternalKeyKind((*[manual.MaxArrayLen]byte)(ptr)[unshared-8])
keyKind = keyKind & base.InternalKeyKindSSTableInternalObsoleteMask
prefixChanged := false prefixChanged := false
if keyKind == InternalKeyKindSet { if keyKind == InternalKeyKindSet {
if invariants.Enabled && value == 0 { if invariants.Enabled && value == 0 {
@@ -1256,8 +1341,12 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
i.key = i.fullKey i.key = i.fullKey
} }
// Manually inlined version of i.decodeInternalKey(i.key). // Manually inlined version of i.decodeInternalKey(i.key).
hiddenPoint := false
if n := len(i.key) - 8; n >= 0 { if n := len(i.key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:]) trailer := binary.LittleEndian.Uint64(i.key[n:])
hiddenPoint = i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = i.key[:n:n] i.ikey.UserKey = i.key[:n:n]
if i.globalSeqNum != 0 { if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum) i.ikey.SetSeqNum(i.globalSeqNum)
@@ -1273,6 +1362,9 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
} }
if prefixChanged || i.cmp(i.ikey.UserKey, succKey) >= 0 { if prefixChanged || i.cmp(i.ikey.UserKey, succKey) >= 0 {
// Prefix has changed. // Prefix has changed.
if hiddenPoint {
return i.Next()
}
if invariants.Enabled && !i.lazyValueHandling.hasValuePrefix { if invariants.Enabled && !i.lazyValueHandling.hasValuePrefix {
panic(errors.AssertionFailedf("nextPrefixV3 being run for non-v3 sstable")) panic(errors.AssertionFailedf("nextPrefixV3 being run for non-v3 sstable"))
} }
@@ -1294,23 +1386,11 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue)
return i.SeekGE(succKey, base.SeekGEFlagsNone) return i.SeekGE(succKey, base.SeekGEFlagsNone)
} }
// NextPrefix implements (base.InternalIterator).NextPrefix
func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) {
const nextsBeforeSeek = 3
k, v := i.Next()
for j := 1; k != nil && i.cmp(k.UserKey, succKey) < 0; j++ {
if j >= nextsBeforeSeek {
return i.SeekGE(succKey, base.SeekGEFlagsNone)
}
k, v = i.Next()
}
return k, v
}
// Prev implements internalIterator.Prev, as documented in the pebble // Prev implements internalIterator.Prev, as documented in the pebble
// package. // package.
func (i *blockIter) Prev() (*InternalKey, base.LazyValue) { func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
if n := len(i.cached) - 1; n >= 0 { start:
for n := len(i.cached) - 1; n >= 0; n-- {
i.nextOffset = i.offset i.nextOffset = i.offset
e := &i.cached[n] e := &i.cached[n]
i.offset = e.offset i.offset = e.offset
@@ -1318,7 +1398,13 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
// Manually inlined version of i.decodeInternalKey(i.key). // Manually inlined version of i.decodeInternalKey(i.key).
i.key = i.cachedBuf[e.keyStart:e.keyEnd] i.key = i.cachedBuf[e.keyStart:e.keyEnd]
if n := len(i.key) - 8; n >= 0 { if n := len(i.key) - 8; n >= 0 {
i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:]) trailer := binary.LittleEndian.Uint64(i.key[n:])
hiddenPoint := i.hideObsoletePoints &&
(trailer&trailerObsoleteBit != 0)
if hiddenPoint {
continue
}
i.ikey.Trailer = trailer & trailerObsoleteMask
i.ikey.UserKey = i.key[:n:n] i.ikey.UserKey = i.key[:n:n]
if i.globalSeqNum != 0 { if i.globalSeqNum != 0 {
i.ikey.SetSeqNum(i.globalSeqNum) i.ikey.SetSeqNum(i.globalSeqNum)
@@ -1360,6 +1446,8 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
// index ≤ h < upper // index ≤ h < upper
offset := decodeRestart(i.data[i.restarts+4*h:]) offset := decodeRestart(i.data[i.restarts+4*h:])
if offset < targetOffset { if offset < targetOffset {
// Looking for the first restart that has offset >= targetOffset, so
// ignore h and earlier.
index = h + 1 // preserves f(i-1) == false index = h + 1 // preserves f(i-1) == false
} else { } else {
upper = h // preserves f(j) == true upper = h // preserves f(j) == true
@@ -1369,20 +1457,35 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) {
// => answer is index. // => answer is index.
} }
// index is first restart with offset >= targetOffset. Note that
// targetOffset may not be at a restart point since one can call Prev()
// after Next() (so the cache was not populated) and targetOffset refers to
// the current entry. index-1 must have an offset < targetOffset (it can't
// be equal to targetOffset since the binary search would have selected that
// as the index).
i.offset = 0 i.offset = 0
if index > 0 { if index > 0 {
i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) i.offset = decodeRestart(i.data[i.restarts+4*(index-1):])
} }
// TODO(sumeer): why is the else case not an error given targetOffset is a
// valid offset.
i.readEntry() i.readEntry()
// We stop when i.nextOffset == targetOffset since the targetOffset is the
// entry we are stepping back from, and we don't need to cache the entry
// before it, since it is the candidate to return.
for i.nextOffset < targetOffset { for i.nextOffset < targetOffset {
i.cacheEntry() i.cacheEntry()
i.offset = i.nextOffset i.offset = i.nextOffset
i.readEntry() i.readEntry()
} }
i.decodeInternalKey(i.key) hiddenPoint := i.decodeInternalKey(i.key)
if hiddenPoint {
// Use the cache.
goto start
}
if !i.lazyValueHandling.hasValuePrefix || if !i.lazyValueHandling.hasValuePrefix ||
base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet {
i.lazyValue = base.MakeInPlaceValue(i.val) i.lazyValue = base.MakeInPlaceValue(i.val)
@@ -1455,6 +1558,10 @@ type fragmentBlockIter struct {
err error err error
dir int8 dir int8
closeHook func(i keyspan.FragmentIterator) error closeHook func(i keyspan.FragmentIterator) error
// elideSameSeqnum, if true, returns only the first-occurring (in forward
// order) Key for each sequence number.
elideSameSeqnum bool
} }
func (i *fragmentBlockIter) resetForReuse() fragmentBlockIter { func (i *fragmentBlockIter) resetForReuse() fragmentBlockIter {
@@ -1483,6 +1590,24 @@ func (i *fragmentBlockIter) decodeSpanKeys(k *InternalKey, internalValue []byte)
} }
} }
func (i *fragmentBlockIter) elideKeysOfSameSeqNum() {
if invariants.Enabled {
if !i.elideSameSeqnum || len(i.span.Keys) == 0 {
panic("elideKeysOfSameSeqNum called when it should not be")
}
}
lastSeqNum := i.span.Keys[0].SeqNum()
k := 1
for j := 1; j < len(i.span.Keys); j++ {
if lastSeqNum != i.span.Keys[j].SeqNum() {
lastSeqNum = i.span.Keys[j].SeqNum()
i.span.Keys[k] = i.span.Keys[j]
k++
}
}
i.span.Keys = i.span.Keys[:k]
}
// gatherForward gathers internal keys with identical bounds. Keys defined over // gatherForward gathers internal keys with identical bounds. Keys defined over
// spans of the keyspace are fragmented such that any overlapping key spans have // spans of the keyspace are fragmented such that any overlapping key spans have
// identical bounds. When these spans are persisted to a range deletion or range // identical bounds. When these spans are persisted to a range deletion or range
@@ -1532,6 +1657,9 @@ func (i *fragmentBlockIter) gatherForward(k *InternalKey, lazyValue base.LazyVal
k, lazyValue = i.blockIter.Next() k, lazyValue = i.blockIter.Next()
internalValue = lazyValue.InPlaceValue() internalValue = lazyValue.InPlaceValue()
} }
if i.elideSameSeqnum && len(i.span.Keys) > 0 {
i.elideKeysOfSameSeqNum()
}
// i.blockIter is positioned over the first internal key for the next span. // i.blockIter is positioned over the first internal key for the next span.
return &i.span return &i.span
} }
@@ -1591,6 +1719,9 @@ func (i *fragmentBlockIter) gatherBackward(k *InternalKey, lazyValue base.LazyVa
// Backwards iteration encounters internal keys in the wrong order. // Backwards iteration encounters internal keys in the wrong order.
keyspan.SortKeysByTrailer(&i.span.Keys) keyspan.SortKeysByTrailer(&i.span.Keys)
if i.elideSameSeqnum && len(i.span.Keys) > 0 {
i.elideKeysOfSameSeqNum()
}
return &i.span return &i.span
} }

View File

@@ -117,7 +117,7 @@ type BlockPropertyCollector interface {
} }
// SuffixReplaceableBlockCollector is an extension to the BlockPropertyCollector // SuffixReplaceableBlockCollector is an extension to the BlockPropertyCollector
// interface that allows a block property collector to indicate the it supports // interface that allows a block property collector to indicate that it supports
// being *updated* during suffix replacement, i.e. when an existing SST in which // being *updated* during suffix replacement, i.e. when an existing SST in which
// all keys have the same key suffix is updated to have a new suffix. // all keys have the same key suffix is updated to have a new suffix.
// //

View File

@@ -24,11 +24,161 @@ const (
TableFormatPebblev1 // Block properties. TableFormatPebblev1 // Block properties.
TableFormatPebblev2 // Range keys. TableFormatPebblev2 // Range keys.
TableFormatPebblev3 // Value blocks. TableFormatPebblev3 // Value blocks.
TableFormatPebblev4 // Reserved. TableFormatPebblev4 // DELSIZED tombstones.
TableFormatMax = TableFormatPebblev4 TableFormatMax = TableFormatPebblev4
) )
// TableFormatPebblev4, in addition to DELSIZED, introduces the use of
// InternalKeyKindSSTableInternalObsoleteBit.
//
// 1. Motivation
//
// We have various related problems caused by Pebble snapshots:
//
// - P1: RANGEDELs that delete points in the same sstable, but the points
// happen to not get deleted during compactions because of an open snapshot.
// This causes very expensive iteration, that has been observed in
// production deployments
//
// - P2: When iterating over a foreign sstable (in disaggregated storage), we
// need to do (a) point collapsing to expose at most one point per user key,
// (b) apply RANGEDELs in the sstable to hide deleted points in the same
// sstable. This per-sstable point collapsing iteration needs to be very
// efficient (ideally as efficient from a CPU perspective as iteration over
// regular sstables) since foreign sstables can be very long-lived -- one of
// the goals of disaggregated storage is to scale compute and disk bandwidth
// resources as a function of the hot (from a write perspective) data and
// not the whole data, so we don't want to have to rewrite foreign sstables
// solely to improve read performance.
//
// The ideal solution for P2 would allow user-facing reads to utilize the
// existing SST iterators (with slight modifications) and with no loss of
// efficiency. And for P1 and P2 we would like to skip whole blocks of
// overwritten/deleted points. Even when we can't skip whole blocks, avoiding
// key comparisons at iteration time to discover what points are deleted is
// very desirable, since keys can be long.
//
// We observe that:
//
// - Reads:
// - All user-facing reads in CockroachDB use iterators over the DB, hence
// have a higher read seqnum than all sstables (there are some rare cases
// that can violate this, but those are not important from a performance
// optimization perspective).
//
// - Certain internal-facing reads in CockroachDB use snapshots, but the
// snapshots are shortlived enough that most L5 and L6 sstables will have
// all seqnums lower than the snapshot seqnum.
//
// - Writes:
// - We already do key comparisons between points when writing the sstable
// to ensure that the sstable invariant (monotonically increasing internal
// keys) is not violated. So we know which points share the same userkey,
// and thereby which points are obsolete because there is a more recent
// point in the same sstable.
//
// - The compactionIter knows which point id deleted by a RANGEDEL even if
// the point does need to be written because of a snapshot.
//
// So this known information can be encoded in the sstable at write time and
// utilized for optimized reading.
//
// 2. Solution
//
// We primarily scope the solution to the following point kinds: SET,
// SETWITHDEL, DEL, DELSIZED, SINGLEDEL. These are the ones marked locally
// obsolete, i.e., obsolete within the sstable, and we can guarantee that at
// most one point will be exposed per user key. MERGE keys create more
// complexity: MERGE followed by MERGE causes multiple keys to not be
// obsolete. Same applies for MERGE followed by SET/SETWITHDEL. Note that:
//
// - For regular sst iteration, the obsolete marking is a performance
// optimization, and multiple keys for the same userkey can be handled by
// higher layers in the iterator tree (specifically pebble.Iterator).
//
// - For foreign sst iteration, we disallow MERGEs to be written to such
// shared ssts (details below).
//
// The key kinds are marked with an obsolete bit
// (InternalKeyKindSSTableInternalObsoleteBit) when the key-value pair is
// obsolete. This marking is done within blockWriter, based on information
// passed to it by Writer. In turn, Writer uses a combination of key
// comparisons, and information provided by compactionIter to decide whether a
// key-value pair is obsolete. Additionally, a Pebble-internal
// BlockPropertyCollector (obsoleteKeyBlockPropertyCollector) is used to mark
// blocks where all key-value pairs are obsolete. Since the common case is
// non-obsolete blocks, this block property collector uses the empty byte
// slice to represent a non-obsolete block, which consumes no space in
// BlockHandleWithProperties.Props.
//
// At read time, the obsolete bit is only visible to the blockIter, which can
// be optionally configured to hide obsolete points. This hiding is only
// configured for data block iterators for sstables being read by user-facing
// iterators at a seqnum greater than the max seqnum in the sstable.
// Additionally, when this hiding is configured, a Pebble-internal block
// property filter (obsoleteKeyBlockPropertyFilter), is used to skip whole
// blocks that are obsolete.
//
// 2.1 Correctness
//
// Due to the level invariant, the sequence of seqnums for a user key in a
// sstable represents a contiguous subsequence of the seqnums for the userkey
// across the whole LSM, and is more recent than the seqnums in a sstable in a
// lower level. So exposing exactly one point from a sstable for a userkey
// will also mask the points for the userkey in lower levels. If we expose no
// point, because of RANGEDELs, that RANGEDEL will also mask the points in
// lower levels.
//
// Note that we do not need to do anything special at write time for
// SETWITHDEL and SINGLEDEL. This is because these key kinds are treated
// specially only by compactions, which do not hide obsolete points. For
// regular reads, SETWITHDEL behaves the same as SET and SINGLEDEL behaves the
// same as DEL.
//
// 2.2 Strictness and MERGE
//
// Setting the obsolete bit on point keys is advanced usage, so we support two
// modes, both of which must be truthful when setting the obsolete bit, but
// vary in when they don't set the obsolete bit.
//
// - Non-strict: In this mode, the bit does not need to be set for keys that
// are obsolete. Additionally, any sstable containing MERGE keys can only
// use this mode. An iterator over such an sstable, when configured to
// hideObsoletePoints, can expose multiple internal keys per user key, and
// can expose keys that are deleted by rangedels in the same sstable. This
// is the mode that non-advanced users should use. Pebble without
// disaggregated storage will also use this mode and will best-effort set
// the obsolete bit, to optimize iteration when snapshots have retained many
// obsolete keys.
//
// - Strict: In this mode, every obsolete key must have the obsolete bit set,
// and no MERGE keys are permitted. An iterator over such an sstable, when
// configured to hideObsoletePoints satisfies two properties:
// - S1: will expose at most one internal key per user key, which is the
// most recent one.
// - S2: will never expose keys that are deleted by rangedels in the same
// sstable.
//
// This is the mode for two use cases in disaggregated storage (which will
// exclude parts of the key space that has MERGEs), for levels that contain
// sstables that can become foreign sstables:
// - Pebble compaction output to these levels that can become foreign
// sstables.
//
// - CockroachDB ingest operations that can ingest into the levels that can
// become foreign sstables. Note, these are not sstables corresponding to
// copied data for CockroachDB range snapshots. This case occurs for
// operations like index backfills: these trivially satisfy the strictness
// criteria since they only write one key per userkey.
//
// TODO(sumeer): this latter case is not currently supported, since only
// Writer.AddWithForceObsolete calls are permitted for writing strict
// obsolete sstables. This is done to reduce the likelihood of bugs. One
// simple way to lift this limitation would be to disallow adding any
// RANGEDELs when a Pebble-external writer is trying to construct a strict
// obsolete sstable.
// ParseTableFormat parses the given magic bytes and version into its // ParseTableFormat parses the given magic bytes and version into its
// corresponding internal TableFormat. // corresponding internal TableFormat.
func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) { func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) {

View File

@@ -23,6 +23,8 @@ const (
InternalKeyKindLogData = base.InternalKeyKindLogData InternalKeyKindLogData = base.InternalKeyKindLogData
InternalKeyKindSingleDelete = base.InternalKeyKindSingleDelete InternalKeyKindSingleDelete = base.InternalKeyKindSingleDelete
InternalKeyKindRangeDelete = base.InternalKeyKindRangeDelete InternalKeyKindRangeDelete = base.InternalKeyKindRangeDelete
InternalKeyKindSetWithDelete = base.InternalKeyKindSetWithDelete
InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized
InternalKeyKindMax = base.InternalKeyKindMax InternalKeyKindMax = base.InternalKeyKindMax
InternalKeyKindInvalid = base.InternalKeyKindInvalid InternalKeyKindInvalid = base.InternalKeyKindInvalid
InternalKeySeqNumBatch = base.InternalKeySeqNumBatch InternalKeySeqNumBatch = base.InternalKeySeqNumBatch

View File

@@ -21,6 +21,17 @@ const (
NCompression NCompression
) )
var ignoredInternalProperties = map[string]struct{}{
"rocksdb.column.family.id": {},
"rocksdb.fixed.key.length": {},
"rocksdb.index.key.is.user.key": {},
"rocksdb.index.value.is.delta.encoded": {},
"rocksdb.oldest.key.time": {},
"rocksdb.creation.time": {},
"rocksdb.file.creation.time": {},
"rocksdb.format.version": {},
}
func (c Compression) String() string { func (c Compression) String() string {
switch c { switch c {
case DefaultCompression: case DefaultCompression:
@@ -100,6 +111,9 @@ type ReaderOptions struct {
// The default cache size is a zero-size cache. // The default cache size is a zero-size cache.
Cache *cache.Cache Cache *cache.Cache
// User properties specified in this map will not be added to sst.Properties.UserProperties.
DeniedUserProperties map[string]struct{}
// Comparer defines a total ordering over the space of []byte keys: a 'less // Comparer defines a total ordering over the space of []byte keys: a 'less
// than' relationship. The same comparison algorithm must be used for reads // than' relationship. The same comparison algorithm must be used for reads
// and writes over the lifetime of the DB. // and writes over the lifetime of the DB.
@@ -107,6 +121,9 @@ type ReaderOptions struct {
// The default value uses the same ordering as bytes.Compare. // The default value uses the same ordering as bytes.Compare.
Comparer *Comparer Comparer *Comparer
// Merge defines the Merge function in use for this keyspace.
Merge base.Merge
// Filters is a map from filter policy name to filter policy. It is used for // Filters is a map from filter policy name to filter policy. It is used for
// debugging tools which may be used on multiple databases configured with // debugging tools which may be used on multiple databases configured with
// different filter policies. It is not necessary to populate this filters // different filter policies. It is not necessary to populate this filters
@@ -126,12 +143,18 @@ func (o ReaderOptions) ensureDefaults() ReaderOptions {
if o.Comparer == nil { if o.Comparer == nil {
o.Comparer = base.DefaultComparer o.Comparer = base.DefaultComparer
} }
if o.Merge == nil {
o.Merge = base.DefaultMerger.Merge
}
if o.MergerName == "" { if o.MergerName == "" {
o.MergerName = base.DefaultMerger.Name o.MergerName = base.DefaultMerger.Name
} }
if o.LoggerAndTracer == nil { if o.LoggerAndTracer == nil {
o.LoggerAndTracer = base.NoopLoggerAndTracer{} o.LoggerAndTracer = base.NoopLoggerAndTracer{}
} }
if o.DeniedUserProperties == nil {
o.DeniedUserProperties = ignoredInternalProperties
}
return o return o
} }
@@ -210,6 +233,17 @@ type WriterOptions struct {
// by a wider range of tools and libraries. // by a wider range of tools and libraries.
TableFormat TableFormat TableFormat TableFormat
// IsStrictObsolete is only relevant for >= TableFormatPebblev4. See comment
// in format.go. Must be false if format < TableFormatPebblev4.
//
// TODO(bilal): set this when writing shared ssts.
IsStrictObsolete bool
// WritingToLowestLevel is only relevant for >= TableFormatPebblev4. It is
// used to set the obsolete bit on DEL/DELSIZED/SINGLEDEL if they are the
// youngest for a userkey.
WritingToLowestLevel bool
// TablePropertyCollectors is a list of TablePropertyCollector creation // TablePropertyCollectors is a list of TablePropertyCollector creation
// functions. A new TablePropertyCollector is created for each sstable built // functions. A new TablePropertyCollector is created for each sstable built
// and lives for the lifetime of the table. // and lives for the lifetime of the table.

View File

@@ -23,14 +23,6 @@ var propTagMap = make(map[string]reflect.StructField)
var propBoolTrue = []byte{'1'} var propBoolTrue = []byte{'1'}
var propBoolFalse = []byte{'0'} var propBoolFalse = []byte{'0'}
var columnFamilyIDField = func() reflect.StructField {
f, ok := reflect.TypeOf(Properties{}).FieldByName("ColumnFamilyID")
if !ok {
panic("Properties.ColumnFamilyID field not found")
}
return f
}()
var propOffsetTagMap = make(map[uintptr]string) var propOffsetTagMap = make(map[uintptr]string)
func init() { func init() {
@@ -56,51 +48,35 @@ func init() {
// automatically populated during sstable creation and load from the properties // automatically populated during sstable creation and load from the properties
// meta block when an sstable is opened. // meta block when an sstable is opened.
type Properties struct { type Properties struct {
// ID of column family for this SST file, corresponding to the CF identified
// by column_family_name.
ColumnFamilyID uint64 `prop:"rocksdb.column.family.id"`
// Name of the column family with which this SST file is associated. Empty if
// the column family is unknown.
ColumnFamilyName string `prop:"rocksdb.column.family.name"`
// The name of the comparer used in this table. // The name of the comparer used in this table.
ComparerName string `prop:"rocksdb.comparator"` ComparerName string `prop:"rocksdb.comparator"`
// The compression algorithm used to compress blocks. // The compression algorithm used to compress blocks.
CompressionName string `prop:"rocksdb.compression"` CompressionName string `prop:"rocksdb.compression"`
// The compression options used to compress blocks. // The compression options used to compress blocks.
CompressionOptions string `prop:"rocksdb.compression_options"` CompressionOptions string `prop:"rocksdb.compression_options"`
// The time when the SST file was created. Since SST files are immutable,
// this is equivalent to last modified time.
CreationTime uint64 `prop:"rocksdb.creation.time"`
// The total size of all data blocks. // The total size of all data blocks.
DataSize uint64 `prop:"rocksdb.data.size"` DataSize uint64 `prop:"rocksdb.data.size"`
// The external sstable version format. Version 2 is the one RocksDB has been // The external sstable version format. Version 2 is the one RocksDB has been
// using since 5.13. RocksDB only uses the global sequence number for an // using since 5.13. RocksDB only uses the global sequence number for an
// sstable if this property has been set. // sstable if this property has been set.
ExternalFormatVersion uint32 `prop:"rocksdb.external_sst_file.version"` ExternalFormatVersion uint32 `prop:"rocksdb.external_sst_file.version"`
// Actual SST file creation time. 0 means unknown.
FileCreationTime uint64 `prop:"rocksdb.file.creation.time"`
// The name of the filter policy used in this table. Empty if no filter // The name of the filter policy used in this table. Empty if no filter
// policy is used. // policy is used.
FilterPolicyName string `prop:"rocksdb.filter.policy"` FilterPolicyName string `prop:"rocksdb.filter.policy"`
// The size of filter block. // The size of filter block.
FilterSize uint64 `prop:"rocksdb.filter.size"` FilterSize uint64 `prop:"rocksdb.filter.size"`
// If 0, key is variable length. Otherwise number of bytes for each key.
FixedKeyLen uint64 `prop:"rocksdb.fixed.key.length"`
// Format version, reserved for backward compatibility.
FormatVersion uint64 `prop:"rocksdb.format.version"`
// The global sequence number to use for all entries in the table. Present if // The global sequence number to use for all entries in the table. Present if
// the table was created externally and ingested whole. // the table was created externally and ingested whole.
GlobalSeqNum uint64 `prop:"rocksdb.external_sst_file.global_seqno"` GlobalSeqNum uint64 `prop:"rocksdb.external_sst_file.global_seqno"`
// Whether the index key is user key or an internal key.
IndexKeyIsUserKey uint64 `prop:"rocksdb.index.key.is.user.key"`
// Total number of index partitions if kTwoLevelIndexSearch is used. // Total number of index partitions if kTwoLevelIndexSearch is used.
IndexPartitions uint64 `prop:"rocksdb.index.partitions"` IndexPartitions uint64 `prop:"rocksdb.index.partitions"`
// The size of index block. // The size of index block.
IndexSize uint64 `prop:"rocksdb.index.size"` IndexSize uint64 `prop:"rocksdb.index.size"`
// The index type. TODO(peter): add a more detailed description. // The index type. TODO(peter): add a more detailed description.
IndexType uint32 `prop:"rocksdb.block.based.table.index.type"` IndexType uint32 `prop:"rocksdb.block.based.table.index.type"`
// Whether delta encoding is used to encode the index values. // For formats >= TableFormatPebblev4, this is set to true if the obsolete
IndexValueIsDeltaEncoded uint64 `prop:"rocksdb.index.value.is.delta.encoded"` // bit is strict for all the point keys.
IsStrictObsolete bool `prop:"pebble.obsolete.is_strict"`
// The name of the merger used in this table. Empty if no merger is used. // The name of the merger used in this table. Empty if no merger is used.
MergerName string `prop:"rocksdb.merge.operator"` MergerName string `prop:"rocksdb.merge.operator"`
// The number of blocks in this table. // The number of blocks in this table.
@@ -108,6 +84,9 @@ type Properties struct {
// The number of deletion entries in this table, including both point and // The number of deletion entries in this table, including both point and
// range deletions. // range deletions.
NumDeletions uint64 `prop:"rocksdb.deleted.keys"` NumDeletions uint64 `prop:"rocksdb.deleted.keys"`
// The number of point deletion entries ("tombstones") in this table that
// carry a size hint indicating the size of the value the tombstone deletes.
NumSizedDeletions uint64 `prop:"pebble.num.deletions.sized"`
// The number of entries in this table. // The number of entries in this table.
NumEntries uint64 `prop:"rocksdb.num.entries"` NumEntries uint64 `prop:"rocksdb.num.entries"`
// The number of merge operands in the table. // The number of merge operands in the table.
@@ -124,8 +103,6 @@ type Properties struct {
NumValueBlocks uint64 `prop:"pebble.num.value-blocks"` NumValueBlocks uint64 `prop:"pebble.num.value-blocks"`
// The number of values stored in value blocks. Only serialized if > 0. // The number of values stored in value blocks. Only serialized if > 0.
NumValuesInValueBlocks uint64 `prop:"pebble.num.values.in.value-blocks"` NumValuesInValueBlocks uint64 `prop:"pebble.num.values.in.value-blocks"`
// Timestamp of the earliest key. 0 if unknown.
OldestKeyTime uint64 `prop:"rocksdb.oldest.key.time"`
// The name of the prefix extractor used in this table. Empty if no prefix // The name of the prefix extractor used in this table. Empty if no prefix
// extractor is used. // extractor is used.
PrefixExtractorName string `prop:"rocksdb.prefix.extractor.name"` PrefixExtractorName string `prop:"rocksdb.prefix.extractor.name"`
@@ -139,6 +116,10 @@ type Properties struct {
// Total raw key size of point deletion tombstones. This value is comparable // Total raw key size of point deletion tombstones. This value is comparable
// to RawKeySize. // to RawKeySize.
RawPointTombstoneKeySize uint64 `prop:"pebble.raw.point-tombstone.key.size"` RawPointTombstoneKeySize uint64 `prop:"pebble.raw.point-tombstone.key.size"`
// Sum of the raw value sizes carried by point deletion tombstones
// containing size estimates. See the DeleteSized key kind. This value is
// comparable to Raw{Key,Value}Size.
RawPointTombstoneValueSize uint64 `prop:"pebble.raw.point-tombstone.value.size"`
// Total raw rangekey key size. // Total raw rangekey key size.
RawRangeKeyKeySize uint64 `prop:"pebble.raw.range-key.key.size"` RawRangeKeyKeySize uint64 `prop:"pebble.raw.range-key.key.size"`
// Total raw rangekey value size. // Total raw rangekey value size.
@@ -206,12 +187,7 @@ func (p *Properties) String() string {
case reflect.Uint32: case reflect.Uint32:
fmt.Fprintf(&buf, "%d\n", f.Uint()) fmt.Fprintf(&buf, "%d\n", f.Uint())
case reflect.Uint64: case reflect.Uint64:
u := f.Uint()
if ft.Offset == columnFamilyIDField.Offset && u == math.MaxInt32 {
fmt.Fprintf(&buf, "-\n")
} else {
fmt.Fprintf(&buf, "%d\n", f.Uint()) fmt.Fprintf(&buf, "%d\n", f.Uint())
}
case reflect.String: case reflect.String:
fmt.Fprintf(&buf, "%s\n", f.String()) fmt.Fprintf(&buf, "%s\n", f.String())
default: default:
@@ -229,7 +205,9 @@ func (p *Properties) String() string {
return buf.String() return buf.String()
} }
func (p *Properties) load(b block, blockOffset uint64) error { func (p *Properties) load(
b block, blockOffset uint64, deniedUserProperties map[string]struct{},
) error {
i, err := newRawBlockIter(bytes.Compare, b) i, err := newRawBlockIter(bytes.Compare, b)
if err != nil { if err != nil {
return err return err
@@ -237,8 +215,7 @@ func (p *Properties) load(b block, blockOffset uint64) error {
p.Loaded = make(map[uintptr]struct{}) p.Loaded = make(map[uintptr]struct{})
v := reflect.ValueOf(p).Elem() v := reflect.ValueOf(p).Elem()
for valid := i.First(); valid; valid = i.Next() { for valid := i.First(); valid; valid = i.Next() {
tag := intern.Bytes(i.Key().UserKey) if f, ok := propTagMap[string(i.Key().UserKey)]; ok {
if f, ok := propTagMap[tag]; ok {
p.Loaded[f.Offset] = struct{}{} p.Loaded[f.Offset] = struct{}{}
field := v.FieldByIndex(f.Index) field := v.FieldByIndex(f.Index)
switch f.Type.Kind() { switch f.Type.Kind() {
@@ -248,7 +225,7 @@ func (p *Properties) load(b block, blockOffset uint64) error {
field.SetUint(uint64(binary.LittleEndian.Uint32(i.Value()))) field.SetUint(uint64(binary.LittleEndian.Uint32(i.Value())))
case reflect.Uint64: case reflect.Uint64:
var n uint64 var n uint64
if tag == propGlobalSeqnumName { if string(i.Key().UserKey) == propGlobalSeqnumName {
n = binary.LittleEndian.Uint64(i.Value()) n = binary.LittleEndian.Uint64(i.Value())
} else { } else {
n, _ = binary.Uvarint(i.Value()) n, _ = binary.Uvarint(i.Value())
@@ -264,7 +241,10 @@ func (p *Properties) load(b block, blockOffset uint64) error {
if p.UserProperties == nil { if p.UserProperties == nil {
p.UserProperties = make(map[string]string) p.UserProperties = make(map[string]string)
} }
p.UserProperties[tag] = string(i.Value())
if _, denied := deniedUserProperties[string(i.Key().UserKey)]; !denied {
p.UserProperties[intern.Bytes(i.Key().UserKey)] = string(i.Value())
}
} }
return nil return nil
} }
@@ -306,10 +286,6 @@ func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) {
m[k] = []byte(v) m[k] = []byte(v)
} }
p.saveUvarint(m, unsafe.Offsetof(p.ColumnFamilyID), p.ColumnFamilyID)
if p.ColumnFamilyName != "" {
p.saveString(m, unsafe.Offsetof(p.ColumnFamilyName), p.ColumnFamilyName)
}
if p.ComparerName != "" { if p.ComparerName != "" {
p.saveString(m, unsafe.Offsetof(p.ComparerName), p.ComparerName) p.saveString(m, unsafe.Offsetof(p.ComparerName), p.ComparerName)
} }
@@ -319,44 +295,45 @@ func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) {
if p.CompressionOptions != "" { if p.CompressionOptions != "" {
p.saveString(m, unsafe.Offsetof(p.CompressionOptions), p.CompressionOptions) p.saveString(m, unsafe.Offsetof(p.CompressionOptions), p.CompressionOptions)
} }
p.saveUvarint(m, unsafe.Offsetof(p.CreationTime), p.CreationTime)
p.saveUvarint(m, unsafe.Offsetof(p.DataSize), p.DataSize) p.saveUvarint(m, unsafe.Offsetof(p.DataSize), p.DataSize)
if p.ExternalFormatVersion != 0 { if p.ExternalFormatVersion != 0 {
p.saveUint32(m, unsafe.Offsetof(p.ExternalFormatVersion), p.ExternalFormatVersion) p.saveUint32(m, unsafe.Offsetof(p.ExternalFormatVersion), p.ExternalFormatVersion)
p.saveUint64(m, unsafe.Offsetof(p.GlobalSeqNum), p.GlobalSeqNum) p.saveUint64(m, unsafe.Offsetof(p.GlobalSeqNum), p.GlobalSeqNum)
} }
if p.FileCreationTime > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.FileCreationTime), p.FileCreationTime)
}
if p.FilterPolicyName != "" { if p.FilterPolicyName != "" {
p.saveString(m, unsafe.Offsetof(p.FilterPolicyName), p.FilterPolicyName) p.saveString(m, unsafe.Offsetof(p.FilterPolicyName), p.FilterPolicyName)
} }
p.saveUvarint(m, unsafe.Offsetof(p.FilterSize), p.FilterSize) p.saveUvarint(m, unsafe.Offsetof(p.FilterSize), p.FilterSize)
p.saveUvarint(m, unsafe.Offsetof(p.FixedKeyLen), p.FixedKeyLen)
p.saveUvarint(m, unsafe.Offsetof(p.FormatVersion), p.FormatVersion)
p.saveUvarint(m, unsafe.Offsetof(p.IndexKeyIsUserKey), p.IndexKeyIsUserKey)
if p.IndexPartitions != 0 { if p.IndexPartitions != 0 {
p.saveUvarint(m, unsafe.Offsetof(p.IndexPartitions), p.IndexPartitions) p.saveUvarint(m, unsafe.Offsetof(p.IndexPartitions), p.IndexPartitions)
p.saveUvarint(m, unsafe.Offsetof(p.TopLevelIndexSize), p.TopLevelIndexSize) p.saveUvarint(m, unsafe.Offsetof(p.TopLevelIndexSize), p.TopLevelIndexSize)
} }
p.saveUvarint(m, unsafe.Offsetof(p.IndexSize), p.IndexSize) p.saveUvarint(m, unsafe.Offsetof(p.IndexSize), p.IndexSize)
p.saveUint32(m, unsafe.Offsetof(p.IndexType), p.IndexType) p.saveUint32(m, unsafe.Offsetof(p.IndexType), p.IndexType)
p.saveUvarint(m, unsafe.Offsetof(p.IndexValueIsDeltaEncoded), p.IndexValueIsDeltaEncoded) if p.IsStrictObsolete {
p.saveBool(m, unsafe.Offsetof(p.IsStrictObsolete), p.IsStrictObsolete)
}
if p.MergerName != "" { if p.MergerName != "" {
p.saveString(m, unsafe.Offsetof(p.MergerName), p.MergerName) p.saveString(m, unsafe.Offsetof(p.MergerName), p.MergerName)
} }
p.saveUvarint(m, unsafe.Offsetof(p.NumDataBlocks), p.NumDataBlocks) p.saveUvarint(m, unsafe.Offsetof(p.NumDataBlocks), p.NumDataBlocks)
p.saveUvarint(m, unsafe.Offsetof(p.NumEntries), p.NumEntries) p.saveUvarint(m, unsafe.Offsetof(p.NumEntries), p.NumEntries)
p.saveUvarint(m, unsafe.Offsetof(p.NumDeletions), p.NumDeletions) p.saveUvarint(m, unsafe.Offsetof(p.NumDeletions), p.NumDeletions)
if p.NumSizedDeletions > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.NumSizedDeletions), p.NumSizedDeletions)
}
p.saveUvarint(m, unsafe.Offsetof(p.NumMergeOperands), p.NumMergeOperands) p.saveUvarint(m, unsafe.Offsetof(p.NumMergeOperands), p.NumMergeOperands)
p.saveUvarint(m, unsafe.Offsetof(p.NumRangeDeletions), p.NumRangeDeletions) p.saveUvarint(m, unsafe.Offsetof(p.NumRangeDeletions), p.NumRangeDeletions)
// NB: We only write out the RawTombstoneKeySize for Pebble formats. This // NB: We only write out some properties for Pebble formats. This isn't
// isn't strictly necessary because unrecognized properties are interpreted // strictly necessary because unrecognized properties are interpreted as
// as user-defined properties, however writing them prevents byte-for-byte // user-defined properties, however writing them prevents byte-for-byte
// equivalence with RocksDB files that some of our testing requires. // equivalence with RocksDB files that some of our testing requires.
if p.RawPointTombstoneKeySize > 0 && tblFormat >= TableFormatPebblev1 { if p.RawPointTombstoneKeySize > 0 && tblFormat >= TableFormatPebblev1 {
p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneKeySize), p.RawPointTombstoneKeySize) p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneKeySize), p.RawPointTombstoneKeySize)
} }
if p.RawPointTombstoneValueSize > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneValueSize), p.RawPointTombstoneValueSize)
}
if p.NumRangeKeys() > 0 { if p.NumRangeKeys() > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeyDels), p.NumRangeKeyDels) p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeyDels), p.NumRangeKeyDels)
p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeySets), p.NumRangeKeySets) p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeySets), p.NumRangeKeySets)
@@ -370,7 +347,6 @@ func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) {
if p.NumValuesInValueBlocks > 0 { if p.NumValuesInValueBlocks > 0 {
p.saveUvarint(m, unsafe.Offsetof(p.NumValuesInValueBlocks), p.NumValuesInValueBlocks) p.saveUvarint(m, unsafe.Offsetof(p.NumValuesInValueBlocks), p.NumValuesInValueBlocks)
} }
p.saveUvarint(m, unsafe.Offsetof(p.OldestKeyTime), p.OldestKeyTime)
if p.PrefixExtractorName != "" { if p.PrefixExtractorName != "" {
p.saveString(m, unsafe.Offsetof(p.PrefixExtractorName), p.PrefixExtractorName) p.saveString(m, unsafe.Offsetof(p.PrefixExtractorName), p.PrefixExtractorName)
} }
@@ -390,6 +366,16 @@ func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) {
} }
p.saveBool(m, unsafe.Offsetof(p.WholeKeyFiltering), p.WholeKeyFiltering) p.saveBool(m, unsafe.Offsetof(p.WholeKeyFiltering), p.WholeKeyFiltering)
if tblFormat < TableFormatPebblev1 {
m["rocksdb.column.family.id"] = binary.AppendUvarint([]byte(nil), math.MaxInt32)
m["rocksdb.fixed.key.length"] = []byte{0x00}
m["rocksdb.index.key.is.user.key"] = []byte{0x00}
m["rocksdb.index.value.is.delta.encoded"] = []byte{0x00}
m["rocksdb.oldest.key.time"] = []byte{0x00}
m["rocksdb.creation.time"] = []byte{0x00}
m["rocksdb.format.version"] = []byte{0x00}
}
keys := make([]string, 0, len(m)) keys := make([]string, 0, len(m))
for key := range m { for key := range m {
keys = append(keys, key) keys = append(keys, key)

View File

@@ -323,6 +323,8 @@ type singleLevelIterator struct {
// is high). // is high).
useFilter bool useFilter bool
lastBloomFilterMatched bool lastBloomFilterMatched bool
hideObsoletePoints bool
} }
// singleLevelIterator implements the base.InternalIterator interface. // singleLevelIterator implements the base.InternalIterator interface.
@@ -404,7 +406,7 @@ func (i *singleLevelIterator) init(
v *virtualState, v *virtualState,
lower, upper []byte, lower, upper []byte,
filterer *BlockPropertiesFilterer, filterer *BlockPropertiesFilterer,
useFilter bool, useFilter, hideObsoletePoints bool,
stats *base.InternalIteratorStats, stats *base.InternalIteratorStats,
rp ReaderProvider, rp ReaderProvider,
) error { ) error {
@@ -428,7 +430,8 @@ func (i *singleLevelIterator) init(
i.reader = r i.reader = r
i.cmp = r.Compare i.cmp = r.Compare
i.stats = stats i.stats = stats
err = i.index.initHandle(i.cmp, indexH, r.Properties.GlobalSeqNum) i.hideObsoletePoints = hideObsoletePoints
err = i.index.initHandle(i.cmp, indexH, r.Properties.GlobalSeqNum, false)
if err != nil { if err != nil {
// blockIter.Close releases indexH and always returns a nil error // blockIter.Close releases indexH and always returns a nil error
_ = i.index.Close() _ = i.index.Close()
@@ -464,9 +467,9 @@ func (i *singleLevelIterator) init(
// setupForCompaction sets up the singleLevelIterator for use with compactionIter. // setupForCompaction sets up the singleLevelIterator for use with compactionIter.
// Currently, it skips readahead ramp-up. It should be called after init is called. // Currently, it skips readahead ramp-up. It should be called after init is called.
func (i *singleLevelIterator) setupForCompaction() { func (i *singleLevelIterator) setupForCompaction() {
i.dataRH.MaxReadahead() i.dataRH.SetupForCompaction()
if i.vbRH != nil { if i.vbRH != nil {
i.vbRH.MaxReadahead() i.vbRH.SetupForCompaction()
} }
} }
@@ -566,7 +569,7 @@ func (i *singleLevelIterator) loadBlock(dir int8) loadBlockResult {
i.err = err i.err = err
return loadBlockFailed return loadBlockFailed
} }
i.err = i.data.initHandle(i.cmp, block, i.reader.Properties.GlobalSeqNum) i.err = i.data.initHandle(i.cmp, block, i.reader.Properties.GlobalSeqNum, i.hideObsoletePoints)
if i.err != nil { if i.err != nil {
// The block is partially loaded, and we don't want it to appear valid. // The block is partially loaded, and we don't want it to appear valid.
i.data.invalidate() i.data.invalidate()
@@ -653,12 +656,13 @@ func (i *singleLevelIterator) resolveMaybeExcluded(dir int8) intersectsResult {
} }
func (i *singleLevelIterator) initBoundsForAlreadyLoadedBlock() { func (i *singleLevelIterator) initBoundsForAlreadyLoadedBlock() {
if i.data.firstKey.UserKey == nil { if i.data.getFirstUserKey() == nil {
panic("initBoundsForAlreadyLoadedBlock must not be called on empty or corrupted block") panic("initBoundsForAlreadyLoadedBlock must not be called on empty or corrupted block")
} }
i.blockLower = i.lower i.blockLower = i.lower
if i.blockLower != nil { if i.blockLower != nil {
if i.data.firstKey.UserKey != nil && i.cmp(i.blockLower, i.data.firstKey.UserKey) < 0 { firstUserKey := i.data.getFirstUserKey()
if firstUserKey != nil && i.cmp(i.blockLower, firstUserKey) < 0 {
// The lower-bound is less than the first key in the block. No need // The lower-bound is less than the first key in the block. No need
// to check the lower-bound again for this block. // to check the lower-bound again for this block.
i.blockLower = nil i.blockLower = nil
@@ -1066,7 +1070,7 @@ func (i *singleLevelIterator) SeekLT(
var dontSeekWithinBlock bool var dontSeekWithinBlock bool
if !i.data.isDataInvalidated() && !i.index.isDataInvalidated() && i.data.valid() && i.index.valid() && if !i.data.isDataInvalidated() && !i.index.isDataInvalidated() && i.data.valid() && i.index.valid() &&
boundsCmp < 0 && i.cmp(i.data.firstKey.UserKey, key) < 0 { boundsCmp < 0 && i.cmp(i.data.getFirstUserKey(), key) < 0 {
// Fast-path: The bounds have moved backward, and this SeekLT is // Fast-path: The bounds have moved backward, and this SeekLT is
// respecting the upper bound (guaranteed by Iterator). We know that // respecting the upper bound (guaranteed by Iterator). We know that
// the iterator must already be positioned within or just outside the // the iterator must already be positioned within or just outside the
@@ -1314,7 +1318,7 @@ func (i *singleLevelIterator) NextPrefix(succKey []byte) (*InternalKey, base.Laz
if i.err != nil { if i.err != nil {
return nil, base.LazyValue{} return nil, base.LazyValue{}
} }
if key, val := i.data.nextPrefix(succKey); key != nil { if key, val := i.data.NextPrefix(succKey); key != nil {
if i.blockUpper != nil { if i.blockUpper != nil {
cmp := i.cmp(key.UserKey, i.blockUpper) cmp := i.cmp(key.UserKey, i.blockUpper)
if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 {
@@ -1763,8 +1767,7 @@ func (i *twoLevelIterator) loadIndex(dir int8) loadBlockResult {
i.err = err i.err = err
return loadBlockFailed return loadBlockFailed
} }
if i.err = i.index.initHandle( if i.err = i.index.initHandle(i.cmp, indexBlock, i.reader.Properties.GlobalSeqNum, false); i.err == nil {
i.cmp, indexBlock, i.reader.Properties.GlobalSeqNum); i.err == nil {
return loadBlockOK return loadBlockOK
} }
return loadBlockFailed return loadBlockFailed
@@ -1842,7 +1845,7 @@ func (i *twoLevelIterator) init(
v *virtualState, v *virtualState,
lower, upper []byte, lower, upper []byte,
filterer *BlockPropertiesFilterer, filterer *BlockPropertiesFilterer,
useFilter bool, useFilter, hideObsoletePoints bool,
stats *base.InternalIteratorStats, stats *base.InternalIteratorStats,
rp ReaderProvider, rp ReaderProvider,
) error { ) error {
@@ -1867,7 +1870,8 @@ func (i *twoLevelIterator) init(
i.reader = r i.reader = r
i.cmp = r.Compare i.cmp = r.Compare
i.stats = stats i.stats = stats
err = i.topLevelIndex.initHandle(i.cmp, topLevelIndexH, r.Properties.GlobalSeqNum) i.hideObsoletePoints = hideObsoletePoints
err = i.topLevelIndex.initHandle(i.cmp, topLevelIndexH, r.Properties.GlobalSeqNum, false)
if err != nil { if err != nil {
// blockIter.Close releases topLevelIndexH and always returns a nil error // blockIter.Close releases topLevelIndexH and always returns a nil error
_ = i.topLevelIndex.Close() _ = i.topLevelIndex.Close()
@@ -2907,21 +2911,20 @@ func (v *VirtualReader) NewCompactionIter(
return v.reader.newCompactionIter(bytesIterated, rp, &v.vState) return v.reader.newCompactionIter(bytesIterated, rp, &v.vState)
} }
// NewIterWithBlockPropertyFiltersAndContext wraps // NewIterWithBlockPropertyFiltersAndContextEtc wraps
// Reader.NewIterWithBlockPropertyFiltersAndContext. We assume that the passed // Reader.NewIterWithBlockPropertyFiltersAndContext. We assume that the passed
// in [lower, upper) bounds will have at least some overlap with the virtual // in [lower, upper) bounds will have at least some overlap with the virtual
// sstable bounds. No overlap is not currently supported in the iterator. // sstable bounds. No overlap is not currently supported in the iterator.
func (v *VirtualReader) NewIterWithBlockPropertyFiltersAndContext( func (v *VirtualReader) NewIterWithBlockPropertyFiltersAndContextEtc(
ctx context.Context, ctx context.Context,
lower, upper []byte, lower, upper []byte,
filterer *BlockPropertiesFilterer, filterer *BlockPropertiesFilterer,
useFilterBlock bool, hideObsoletePoints, useFilterBlock bool,
stats *base.InternalIteratorStats, stats *base.InternalIteratorStats,
rp ReaderProvider, rp ReaderProvider,
) (Iterator, error) { ) (Iterator, error) {
return v.reader.newIterWithBlockPropertyFiltersAndContext( return v.reader.newIterWithBlockPropertyFiltersAndContext(
ctx, ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, rp, &v.vState,
lower, upper, filterer, useFilterBlock, stats, rp, &v.vState,
) )
} }
@@ -3060,31 +3063,51 @@ func (r *Reader) NewIterWithBlockPropertyFilters(
) (Iterator, error) { ) (Iterator, error) {
return r.newIterWithBlockPropertyFiltersAndContext( return r.newIterWithBlockPropertyFiltersAndContext(
context.Background(), context.Background(),
lower, upper, filterer, useFilterBlock, stats, rp, nil, lower, upper, filterer, false, useFilterBlock, stats, rp, nil,
) )
} }
// NewIterWithBlockPropertyFiltersAndContext is similar to // NewIterWithBlockPropertyFiltersAndContextEtc is similar to
// NewIterWithBlockPropertyFilters and additionally accepts a context for // NewIterWithBlockPropertyFilters and additionally accepts a context for
// tracing. // tracing.
func (r *Reader) NewIterWithBlockPropertyFiltersAndContext( //
// If hideObsoletePoints, the callee assumes that filterer already includes
// obsoleteKeyBlockPropertyFilter. The caller can satisfy this contract by
// first calling TryAddBlockPropertyFilterForHideObsoletePoints.
func (r *Reader) NewIterWithBlockPropertyFiltersAndContextEtc(
ctx context.Context, ctx context.Context,
lower, upper []byte, lower, upper []byte,
filterer *BlockPropertiesFilterer, filterer *BlockPropertiesFilterer,
useFilterBlock bool, hideObsoletePoints, useFilterBlock bool,
stats *base.InternalIteratorStats, stats *base.InternalIteratorStats,
rp ReaderProvider, rp ReaderProvider,
) (Iterator, error) { ) (Iterator, error) {
return r.newIterWithBlockPropertyFiltersAndContext( return r.newIterWithBlockPropertyFiltersAndContext(
ctx, ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, rp, nil,
lower, upper, filterer, useFilterBlock, stats, rp, nil,
) )
} }
// TryAddBlockPropertyFilterForHideObsoletePoints is expected to be called
// before the call to NewIterWithBlockPropertyFiltersAndContextEtc, to get the
// value of hideObsoletePoints and potentially add a block property filter.
func (r *Reader) TryAddBlockPropertyFilterForHideObsoletePoints(
snapshotForHideObsoletePoints uint64,
fileLargestSeqNum uint64,
pointKeyFilters []BlockPropertyFilter,
) (hideObsoletePoints bool, filters []BlockPropertyFilter) {
hideObsoletePoints = r.tableFormat >= TableFormatPebblev4 &&
snapshotForHideObsoletePoints > fileLargestSeqNum
if hideObsoletePoints {
pointKeyFilters = append(pointKeyFilters, obsoleteKeyBlockPropertyFilter{})
}
return hideObsoletePoints, pointKeyFilters
}
func (r *Reader) newIterWithBlockPropertyFiltersAndContext( func (r *Reader) newIterWithBlockPropertyFiltersAndContext(
ctx context.Context, ctx context.Context,
lower, upper []byte, lower, upper []byte,
filterer *BlockPropertiesFilterer, filterer *BlockPropertiesFilterer,
hideObsoletePoints bool,
useFilterBlock bool, useFilterBlock bool,
stats *base.InternalIteratorStats, stats *base.InternalIteratorStats,
rp ReaderProvider, rp ReaderProvider,
@@ -3095,7 +3118,7 @@ func (r *Reader) newIterWithBlockPropertyFiltersAndContext(
// until the final iterator closes. // until the final iterator closes.
if r.Properties.IndexType == twoLevelIndex { if r.Properties.IndexType == twoLevelIndex {
i := twoLevelIterPool.Get().(*twoLevelIterator) i := twoLevelIterPool.Get().(*twoLevelIterator)
err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, stats, rp) err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, hideObsoletePoints, stats, rp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -3103,7 +3126,7 @@ func (r *Reader) newIterWithBlockPropertyFiltersAndContext(
} }
i := singleLevelIterPool.Get().(*singleLevelIterator) i := singleLevelIterPool.Get().(*singleLevelIterator)
err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, stats, rp) err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, hideObsoletePoints, stats, rp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -3135,7 +3158,8 @@ func (r *Reader) newCompactionIter(
err := i.init( err := i.init(
context.Background(), context.Background(),
r, v, nil /* lower */, nil /* upper */, nil, r, v, nil /* lower */, nil /* upper */, nil,
false /* useFilter */, nil /* stats */, rp, false /* useFilter */, false, /* hideObsoletePoints */
nil /* stats */, rp,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -3149,7 +3173,8 @@ func (r *Reader) newCompactionIter(
i := singleLevelIterPool.Get().(*singleLevelIterator) i := singleLevelIterPool.Get().(*singleLevelIterator)
err := i.init( err := i.init(
context.Background(), r, v, nil /* lower */, nil, /* upper */ context.Background(), r, v, nil /* lower */, nil, /* upper */
nil, false /* useFilter */, nil /* stats */, rp, nil, false /* useFilter */, false, /* hideObsoletePoints */
nil /* stats */, rp,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -3175,8 +3200,8 @@ func (r *Reader) NewRawRangeDelIter() (keyspan.FragmentIterator, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
i := &fragmentBlockIter{} i := &fragmentBlockIter{elideSameSeqnum: true}
if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum); err != nil { if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum, false); err != nil {
return nil, err return nil, err
} }
return i, nil return i, nil
@@ -3197,7 +3222,7 @@ func (r *Reader) NewRawRangeKeyIter() (keyspan.FragmentIterator, error) {
return nil, err return nil, err
} }
i := rangeKeyFragmentBlockIterPool.Get().(*rangeKeyFragmentBlockIter) i := rangeKeyFragmentBlockIterPool.Get().(*rangeKeyFragmentBlockIter)
if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum); err != nil { if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum, false); err != nil {
return nil, err return nil, err
} }
return i, nil return i, nil
@@ -3357,7 +3382,7 @@ func (r *Reader) transformRangeDelV1(b []byte) ([]byte, error) {
// tombstones. We need properly fragmented and sorted range tombstones in // tombstones. We need properly fragmented and sorted range tombstones in
// order to serve from them directly. // order to serve from them directly.
iter := &blockIter{} iter := &blockIter{}
if err := iter.init(r.Compare, b, r.Properties.GlobalSeqNum); err != nil { if err := iter.init(r.Compare, b, r.Properties.GlobalSeqNum, false); err != nil {
return nil, err return nil, err
} }
var tombstones []keyspan.Span var tombstones []keyspan.Span
@@ -3444,7 +3469,7 @@ func (r *Reader) readMetaindex(metaindexBH BlockHandle) error {
return err return err
} }
r.propertiesBH = bh r.propertiesBH = bh
err := r.Properties.load(b.Get(), bh.Offset) err := r.Properties.load(b.Get(), bh.Offset, r.opts.DeniedUserProperties)
b.Release() b.Release()
if err != nil { if err != nil {
return err return err
@@ -3549,7 +3574,8 @@ func (r *Reader) Layout() (*Layout, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := iter.init(r.Compare, subIndex.Get(), 0 /* globalSeqNum */); err != nil { if err := iter.init(r.Compare, subIndex.Get(), 0, /* globalSeqNum */
false /* hideObsoletePoints */); err != nil {
return nil, err return nil, err
} }
for key, value := iter.First(); key != nil; key, value = iter.Next() { for key, value := iter.First(); key != nil; key, value = iter.Next() {
@@ -3867,6 +3893,7 @@ func NewReader(f objstorage.Readable, o ReaderOptions, extraOpts ...ReaderOption
if r.err != nil { if r.err != nil {
return nil, r.Close() return nil, r.Close()
} }
return r, nil return r, nil
} }

View File

@@ -43,6 +43,9 @@ func RewriteKeySuffixes(
// modification as they are not affected by the suffix, while block and table // modification as they are not affected by the suffix, while block and table
// properties are only minimally recomputed. // properties are only minimally recomputed.
// //
// TODO(sumeer): document limitations, if any, due to this limited
// re-computation of properties (is there any loss of fidelity?).
//
// Any block and table property collectors configured in the WriterOptions must // Any block and table property collectors configured in the WriterOptions must
// implement SuffixReplaceableTableCollector/SuffixReplaceableBlockCollector. // implement SuffixReplaceableTableCollector/SuffixReplaceableBlockCollector.
// //
@@ -50,6 +53,26 @@ func RewriteKeySuffixes(
// same TableFormat as the input, which is returned in case the caller wants // same TableFormat as the input, which is returned in case the caller wants
// to do some error checking. Suffix rewriting is meant to be efficient, and // to do some error checking. Suffix rewriting is meant to be efficient, and
// allowing changes in the TableFormat detracts from that efficiency. // allowing changes in the TableFormat detracts from that efficiency.
//
// Any obsolete bits that key-value pairs may be annotated with are ignored
// and lost during the rewrite. Additionally, the output sstable has the
// pebble.obsolete.is_strict property set to false. These limitations could be
// removed if needed. The current use case for
// RewriteKeySuffixesAndReturnFormat in CockroachDB is for MVCC-compliant file
// ingestion, where these files do not contain RANGEDELs and have one
// key-value pair per userkey -- so they trivially satisfy the strict
// criteria, and we don't need the obsolete bit as a performance optimization.
// For disaggregated storage, strict obsolete sstables are needed for L5 and
// L6, but at the time of writing, we expect such MVCC-compliant file
// ingestion to only ingest into levels L4 and higher. If this changes, we can
// do one of two things to get rid of this limitation:
// - Validate that there are no duplicate userkeys and no RANGEDELs/MERGEs
// in the sstable to be rewritten. Validating no duplicate userkeys is
// non-trivial when rewriting blocks in parallel, so we could encode the
// pre-existing condition in the (existing) SnapshotPinnedKeys property --
// we need to update the external sst writer to calculate and encode this
// property.
// - Preserve the obsolete bit (with changes to the blockIter).
func RewriteKeySuffixesAndReturnFormat( func RewriteKeySuffixesAndReturnFormat(
sst []byte, sst []byte,
rOpts ReaderOptions, rOpts ReaderOptions,
@@ -184,7 +207,7 @@ func rewriteBlocks(
if err != nil { if err != nil {
return err return err
} }
if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum); err != nil { if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum, false); err != nil {
return err return err
} }
@@ -433,8 +456,13 @@ func (c copyFilterWriter) policyName() string { return c.origPolicyName }
// RewriteKeySuffixesViaWriter is similar to RewriteKeySuffixes but uses just a // RewriteKeySuffixesViaWriter is similar to RewriteKeySuffixes but uses just a
// single loop over the Reader that writes each key to the Writer with the new // single loop over the Reader that writes each key to the Writer with the new
// suffix. The is significantly slower than the parallelized rewriter, and does // suffix. The is significantly slower than the parallelized rewriter, and does
// more work to rederive filters, props, etc, however re-doing that work makes // more work to rederive filters, props, etc.
// it less restrictive -- props no longer need to //
// Any obsolete bits that key-value pairs may be annotated with are ignored
// and lost during the rewrite. Some of the obsolete bits may be recreated --
// specifically when there are multiple keys with the same user key.
// Additionally, the output sstable has the pebble.obsolete.is_strict property
// set to false. See the longer comment at RewriteKeySuffixesAndReturnFormat.
func RewriteKeySuffixesViaWriter( func RewriteKeySuffixesViaWriter(
r *Reader, out objstorage.Writable, o WriterOptions, from, to []byte, r *Reader, out objstorage.Writable, o WriterOptions, from, to []byte,
) (*WriterMetadata, error) { ) (*WriterMetadata, error) {
@@ -442,6 +470,7 @@ func RewriteKeySuffixesViaWriter(
return nil, errors.New("a valid splitter is required to rewrite suffixes") return nil, errors.New("a valid splitter is required to rewrite suffixes")
} }
o.IsStrictObsolete = false
w := NewWriter(out, o) w := NewWriter(out, o)
defer func() { defer func() {
if w != nil { if w != nil {
@@ -472,7 +501,7 @@ func RewriteKeySuffixesViaWriter(
if err != nil { if err != nil {
return nil, err return nil, err
} }
if w.addPoint(scratch, val); err != nil { if w.addPoint(scratch, val, false); err != nil {
return nil, err return nil, err
} }
k, v = i.Next() k, v = i.Next()

View File

@@ -160,8 +160,27 @@ The metaindex block also contains block handles as values, with keys being
the names of the meta blocks. the names of the meta blocks.
For a description of value blocks and the meta value index block, see For a description of value blocks and the meta value index block, see
value_block.go value_block.go.
Data blocks have some additional features:
- For TableFormatPebblev3 onwards:
- For SETs, the value has a 1 byte value prefix, which indicates whether the
value is inline, or in a separate value block, and indicates whether the
prefix of the userkey (as defined by split) has changed or not. See
value_block.go for details.
- The most significant bit of the restart points is used to indicate whether
userkey prefix has changed since the last restart point. See the detailed
comment in blockWriter.
- The maximum length of the "shared prefix" when encoding the key, is the
length of the prefix of the userkey (as defined by split) of the previous
key.
- For TableFormatPebblev4 onwards:
- The key kinds may be altered to set the
InternalKeyKindSSTableInternalObsoleteBit if the key-value pair is obsolete
in the context of that sstable (for a reader that reads at a higher seqnum
than the highest seqnum in the sstable). For details, see the comment in
format.go.
*/ */
const ( const (

View File

@@ -132,6 +132,8 @@ type Writer struct {
separator Separator separator Separator
successor Successor successor Successor
tableFormat TableFormat tableFormat TableFormat
isStrictObsolete bool
writingToLowestLevel bool
cache *cache.Cache cache *cache.Cache
restartInterval int restartInterval int
checksumType ChecksumType checksumType ChecksumType
@@ -166,6 +168,7 @@ type Writer struct {
props Properties props Properties
propCollectors []TablePropertyCollector propCollectors []TablePropertyCollector
blockPropCollectors []BlockPropertyCollector blockPropCollectors []BlockPropertyCollector
obsoleteCollector obsoleteKeyBlockPropertyCollector
blockPropsEncoder blockPropertiesEncoder blockPropsEncoder blockPropertiesEncoder
// filter accumulates the filter block. If populated, the filter ingests // filter accumulates the filter block. If populated, the filter ingests
// either the output of w.split (i.e. a prefix extractor) if w.split is not // either the output of w.split (i.e. a prefix extractor) if w.split is not
@@ -215,6 +218,8 @@ type pointKeyInfo struct {
// prefixLen uses w.split, if not nil. Only computed when w.valueBlockWriter // prefixLen uses w.split, if not nil. Only computed when w.valueBlockWriter
// is not nil. // is not nil.
prefixLen int prefixLen int
// True iff the point was marked obsolete.
isObsolete bool
} }
type coordinationState struct { type coordinationState struct {
@@ -673,7 +678,12 @@ func (w *Writer) Set(key, value []byte) error {
if w.err != nil { if w.err != nil {
return w.err return w.err
} }
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindSet), value) if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
// forceObsolete is false based on the assumption that no RANGEDELs in the
// sstable delete the added points.
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindSet), value, false)
} }
// Delete deletes the value for the given key. The sequence number is set to // Delete deletes the value for the given key. The sequence number is set to
@@ -685,7 +695,12 @@ func (w *Writer) Delete(key []byte) error {
if w.err != nil { if w.err != nil {
return w.err return w.err
} }
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindDelete), nil) if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
// forceObsolete is false based on the assumption that no RANGEDELs in the
// sstable delete the added points.
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindDelete), nil, false)
} }
// DeleteRange deletes all of the keys (and values) in the range [start,end) // DeleteRange deletes all of the keys (and values) in the range [start,end)
@@ -711,7 +726,13 @@ func (w *Writer) Merge(key, value []byte) error {
if w.err != nil { if w.err != nil {
return w.err return w.err
} }
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindMerge), value) if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
// forceObsolete is false based on the assumption that no RANGEDELs in the
// sstable that delete the added points. If the user configured this writer
// to be strict-obsolete, addPoint will reject the addition of this MERGE.
return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindMerge), value, false)
} }
// Add adds a key/value pair to the table being written. For a given Writer, // Add adds a key/value pair to the table being written. For a given Writer,
@@ -721,6 +742,24 @@ func (w *Writer) Merge(key, value []byte) error {
// point entries. Additionally, range deletion tombstones must be fragmented // point entries. Additionally, range deletion tombstones must be fragmented
// (i.e. by keyspan.Fragmenter). // (i.e. by keyspan.Fragmenter).
func (w *Writer) Add(key InternalKey, value []byte) error { func (w *Writer) Add(key InternalKey, value []byte) error {
if w.isStrictObsolete {
return errors.Errorf("use AddWithForceObsolete")
}
return w.AddWithForceObsolete(key, value, false)
}
// AddWithForceObsolete must be used when writing a strict-obsolete sstable.
//
// forceObsolete indicates whether the caller has determined that this key is
// obsolete even though it may be the latest point key for this userkey. This
// should be set to true for keys obsoleted by RANGEDELs, and is required for
// strict-obsolete sstables.
//
// Note that there are two properties, S1 and S2 (see comment in format.go)
// that strict-obsolete ssts must satisfy. S2, due to RANGEDELs, is solely the
// responsibility of the caller. S1 is solely the responsibility of the
// callee.
func (w *Writer) AddWithForceObsolete(key InternalKey, value []byte, forceObsolete bool) error {
if w.err != nil { if w.err != nil {
return w.err return w.err
} }
@@ -735,7 +774,7 @@ func (w *Writer) Add(key InternalKey, value []byte) error {
"pebble: range keys must be added via one of the RangeKey* functions") "pebble: range keys must be added via one of the RangeKey* functions")
return w.err return w.err
} }
return w.addPoint(key, value) return w.addPoint(key, value, forceObsolete)
} }
func (w *Writer) makeAddPointDecisionV2(key InternalKey) error { func (w *Writer) makeAddPointDecisionV2(key InternalKey) error {
@@ -745,12 +784,7 @@ func (w *Writer) makeAddPointDecisionV2(key InternalKey) error {
return nil return nil
} }
if !w.disableKeyOrderChecks { if !w.disableKeyOrderChecks {
prevPointUserKey := w.dataBlockBuf.dataBlock.curKey prevPointUserKey := w.dataBlockBuf.dataBlock.getCurUserKey()
n := len(prevPointUserKey) - base.InternalTrailerLen
if n < 0 {
panic("corrupt key in blockWriter buffer")
}
prevPointUserKey = prevPointUserKey[:n:n]
cmpUser := w.compare(prevPointUserKey, key.UserKey) cmpUser := w.compare(prevPointUserKey, key.UserKey)
if cmpUser > 0 || (cmpUser == 0 && prevTrailer <= key.Trailer) { if cmpUser > 0 || (cmpUser == 0 && prevTrailer <= key.Trailer) {
return errors.Errorf( return errors.Errorf(
@@ -762,9 +796,17 @@ func (w *Writer) makeAddPointDecisionV2(key InternalKey) error {
return nil return nil
} }
// REQUIRES: at least one point has been written to the Writer.
func (w *Writer) getLastPointUserKey() []byte {
if w.dataBlockBuf.dataBlock.nEntries == 0 {
panic(errors.AssertionFailedf("no point keys added to writer"))
}
return w.dataBlockBuf.dataBlock.getCurUserKey()
}
func (w *Writer) makeAddPointDecisionV3( func (w *Writer) makeAddPointDecisionV3(
key InternalKey, valueLen int, key InternalKey, valueLen int,
) (setHasSamePrefix bool, writeToValueBlock bool, err error) { ) (setHasSamePrefix bool, writeToValueBlock bool, isObsolete bool, err error) {
prevPointKeyInfo := w.lastPointKeyInfo prevPointKeyInfo := w.lastPointKeyInfo
w.lastPointKeyInfo.userKeyLen = len(key.UserKey) w.lastPointKeyInfo.userKeyLen = len(key.UserKey)
w.lastPointKeyInfo.prefixLen = w.lastPointKeyInfo.userKeyLen w.lastPointKeyInfo.prefixLen = w.lastPointKeyInfo.userKeyLen
@@ -772,18 +814,16 @@ func (w *Writer) makeAddPointDecisionV3(
w.lastPointKeyInfo.prefixLen = w.split(key.UserKey) w.lastPointKeyInfo.prefixLen = w.split(key.UserKey)
} }
w.lastPointKeyInfo.trailer = key.Trailer w.lastPointKeyInfo.trailer = key.Trailer
if w.dataBlockBuf.dataBlock.nEntries == 0 { w.lastPointKeyInfo.isObsolete = false
return false, false, nil if !w.meta.HasPointKeys {
return false, false, false, nil
} }
prevPointUserKey := w.dataBlockBuf.dataBlock.curKey keyKind := base.TrailerKind(key.Trailer)
n := len(prevPointUserKey) - base.InternalTrailerLen prevPointUserKey := w.getLastPointUserKey()
if n < 0 {
panic("corrupt key in blockWriter buffer")
}
prevPointUserKey = prevPointUserKey[:n:n]
prevPointKey := InternalKey{UserKey: prevPointUserKey, Trailer: prevPointKeyInfo.trailer} prevPointKey := InternalKey{UserKey: prevPointUserKey, Trailer: prevPointKeyInfo.trailer}
considerWriteToValueBlock := base.TrailerKind(prevPointKeyInfo.trailer) == InternalKeyKindSet && prevKeyKind := base.TrailerKind(prevPointKeyInfo.trailer)
base.TrailerKind(key.Trailer) == InternalKeyKindSet considerWriteToValueBlock := prevKeyKind == InternalKeyKindSet &&
keyKind == InternalKeyKindSet
if considerWriteToValueBlock && !w.requiredInPlaceValueBound.IsEmpty() { if considerWriteToValueBlock && !w.requiredInPlaceValueBound.IsEmpty() {
keyPrefix := key.UserKey[:w.lastPointKeyInfo.prefixLen] keyPrefix := key.UserKey[:w.lastPointKeyInfo.prefixLen]
cmpUpper := w.compare( cmpUpper := w.compare(
@@ -812,14 +852,59 @@ func (w *Writer) makeAddPointDecisionV3(
} else { } else {
cmpUser = w.compare(prevPointUserKey, key.UserKey) cmpUser = w.compare(prevPointUserKey, key.UserKey)
} }
keyKindIsSetOrMerge := keyKind == InternalKeyKindSet || keyKind == InternalKeyKindSetWithDelete ||
keyKind == InternalKeyKindMerge
keyKindIsPointDelete := keyKind == InternalKeyKindDelete ||
keyKind == InternalKeyKindSingleDelete || keyKind == InternalKeyKindDeleteSized
if !keyKindIsSetOrMerge && !keyKindIsPointDelete {
panic(errors.AssertionFailedf("unexpected key kind %s", keyKind.String()))
}
// If same user key, then the current key is obsolete if any of the
// following is true:
// C1 The prev key was obsolete.
// C2 The prev key was not a MERGE
// C3 The current key is not a SET or SETWITHDELETE or MERGE, then it is
// obsolete. This rule excludes SET, SETWITHDELETE, MERGE, since they
// will be merged with the previous key.
//
// Regardless of whether it is the same user key or not
// C4 The current key is some kind of point delete, and we are writing to
// the lowest level, then it is also obsolete.
//
// C1 ensures that for a user key there is at most one transition from
// !obsolete to obsolete. Consider a user key k, for which the first n keys
// are not obsolete. We consider the various value of n:
//
// n = 0: This happens due to forceObsolete being set by the caller, or due
// to C4. forceObsolete must only be set due a RANGEDEL, and that RANGEDEL
// must also delete all the lower seqnums for the same user key. C4 triggers
// due to a point delete and that deletes all the lower seqnums for the same
// user key.
//
// n = 1: This is the common case. It happens when the first key is not a
// MERGE, or the current key is some kind of point delete.
//
// n > 1: This is due to a sequence of MERGE keys, potentially followed by a
// single SET or SETWITHDELETE.
isObsolete = (cmpUser == 0 &&
(prevKeyKind != InternalKeyKindMerge || prevPointKeyInfo.isObsolete || !keyKindIsSetOrMerge)) ||
(w.writingToLowestLevel &&
(keyKind == InternalKeyKindDelete || keyKind == InternalKeyKindSingleDelete ||
keyKind == InternalKeyKindDeleteSized))
// TODO(sumeer): storing isObsolete SET and SETWITHDEL in value blocks is
// possible, but requires some care in documenting and checking invariants.
// There is code that assumes nothing in value blocks because of single MVCC
// version (those should be ok). We have to ensure setHasSamePrefix is
// correctly initialized here etc.
if !w.disableKeyOrderChecks && if !w.disableKeyOrderChecks &&
(cmpUser > 0 || (cmpUser == 0 && prevPointKeyInfo.trailer <= key.Trailer)) { (cmpUser > 0 || (cmpUser == 0 && prevPointKeyInfo.trailer <= key.Trailer)) {
return false, false, errors.Errorf( return false, false, false, errors.Errorf(
"pebble: keys must be added in strictly increasing order: %s, %s", "pebble: keys must be added in strictly increasing order: %s, %s",
prevPointKey.Pretty(w.formatKey), key.Pretty(w.formatKey)) prevPointKey.Pretty(w.formatKey), key.Pretty(w.formatKey))
} }
if !considerWriteToValueBlock { if !considerWriteToValueBlock {
return false, false, nil return false, false, isObsolete, nil
} }
// NB: it is possible that cmpUser == 0, i.e., these two SETs have identical // NB: it is possible that cmpUser == 0, i.e., these two SETs have identical
// user keys (because of an open snapshot). This should be the rare case. // user keys (because of an open snapshot). This should be the rare case.
@@ -832,19 +917,24 @@ func (w *Writer) makeAddPointDecisionV3(
if considerWriteToValueBlock && valueLen <= tinyValueThreshold { if considerWriteToValueBlock && valueLen <= tinyValueThreshold {
considerWriteToValueBlock = false considerWriteToValueBlock = false
} }
return setHasSamePrefix, considerWriteToValueBlock, nil return setHasSamePrefix, considerWriteToValueBlock, isObsolete, nil
} }
func (w *Writer) addPoint(key InternalKey, value []byte) error { func (w *Writer) addPoint(key InternalKey, value []byte, forceObsolete bool) error {
if w.isStrictObsolete && key.Kind() == InternalKeyKindMerge {
return errors.Errorf("MERGE not supported in a strict-obsolete sstable")
}
var err error var err error
var setHasSameKeyPrefix, writeToValueBlock, addPrefixToValueStoredWithKey bool var setHasSameKeyPrefix, writeToValueBlock, addPrefixToValueStoredWithKey bool
var isObsolete bool
maxSharedKeyLen := len(key.UserKey) maxSharedKeyLen := len(key.UserKey)
if w.valueBlockWriter != nil { if w.valueBlockWriter != nil {
// maxSharedKeyLen is limited to the prefix of the preceding key. If the // maxSharedKeyLen is limited to the prefix of the preceding key. If the
// preceding key was in a different block, then the blockWriter will // preceding key was in a different block, then the blockWriter will
// ignore this maxSharedKeyLen. // ignore this maxSharedKeyLen.
maxSharedKeyLen = w.lastPointKeyInfo.prefixLen maxSharedKeyLen = w.lastPointKeyInfo.prefixLen
setHasSameKeyPrefix, writeToValueBlock, err = w.makeAddPointDecisionV3(key, len(value)) setHasSameKeyPrefix, writeToValueBlock, isObsolete, err =
w.makeAddPointDecisionV3(key, len(value))
addPrefixToValueStoredWithKey = base.TrailerKind(key.Trailer) == InternalKeyKindSet addPrefixToValueStoredWithKey = base.TrailerKind(key.Trailer) == InternalKeyKindSet
} else { } else {
err = w.makeAddPointDecisionV2(key) err = w.makeAddPointDecisionV2(key)
@@ -852,6 +942,8 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error {
if err != nil { if err != nil {
return err return err
} }
isObsolete = w.tableFormat >= TableFormatPebblev4 && (isObsolete || forceObsolete)
w.lastPointKeyInfo.isObsolete = isObsolete
var valueStoredWithKey []byte var valueStoredWithKey []byte
var prefix valuePrefix var prefix valuePrefix
var valueStoredWithKeyLen int var valueStoredWithKeyLen int
@@ -907,16 +999,19 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error {
return err return err
} }
} }
if w.tableFormat >= TableFormatPebblev4 {
w.obsoleteCollector.AddPoint(isObsolete)
}
w.maybeAddToFilter(key.UserKey) w.maybeAddToFilter(key.UserKey)
w.dataBlockBuf.dataBlock.addWithOptionalValuePrefix( w.dataBlockBuf.dataBlock.addWithOptionalValuePrefix(
key, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix, key, isObsolete, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix,
setHasSameKeyPrefix) setHasSameKeyPrefix)
w.meta.updateSeqNum(key.SeqNum()) w.meta.updateSeqNum(key.SeqNum())
if !w.meta.HasPointKeys { if !w.meta.HasPointKeys {
k := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey) k := w.dataBlockBuf.dataBlock.getCurKey()
// NB: We need to ensure that SmallestPoint.UserKey is set, so we create // NB: We need to ensure that SmallestPoint.UserKey is set, so we create
// an InternalKey which is semantically identical to the key, but won't // an InternalKey which is semantically identical to the key, but won't
// have a nil UserKey. We do this, because key.UserKey could be nil, and // have a nil UserKey. We do this, because key.UserKey could be nil, and
@@ -932,7 +1027,22 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error {
switch key.Kind() { switch key.Kind() {
case InternalKeyKindDelete, InternalKeyKindSingleDelete: case InternalKeyKindDelete, InternalKeyKindSingleDelete:
w.props.NumDeletions++ w.props.NumDeletions++
w.props.RawPointTombstoneKeySize += uint64(key.Size()) w.props.RawPointTombstoneKeySize += uint64(len(key.UserKey))
case InternalKeyKindDeleteSized:
var size uint64
if len(value) > 0 {
var n int
size, n = binary.Uvarint(value)
if n <= 0 {
w.err = errors.Newf("%s key's value (%x) does not parse as uvarint",
errors.Safe(key.Kind().String()), value)
return w.err
}
}
w.props.NumDeletions++
w.props.NumSizedDeletions++
w.props.RawPointTombstoneKeySize += uint64(len(key.UserKey))
w.props.RawPointTombstoneValueSize += size
case InternalKeyKindMerge: case InternalKeyKindMerge:
w.props.NumMergeOperands++ w.props.NumMergeOperands++
} }
@@ -953,7 +1063,7 @@ func (w *Writer) addTombstone(key InternalKey, value []byte) error {
if !w.disableKeyOrderChecks && !w.rangeDelV1Format && w.rangeDelBlock.nEntries > 0 { if !w.disableKeyOrderChecks && !w.rangeDelV1Format && w.rangeDelBlock.nEntries > 0 {
// Check that tombstones are being added in fragmented order. If the two // Check that tombstones are being added in fragmented order. If the two
// tombstones overlap, their start and end keys must be identical. // tombstones overlap, their start and end keys must be identical.
prevKey := base.DecodeInternalKey(w.rangeDelBlock.curKey) prevKey := w.rangeDelBlock.getCurKey()
switch c := w.compare(prevKey.UserKey, key.UserKey); { switch c := w.compare(prevKey.UserKey, key.UserKey); {
case c > 0: case c > 0:
w.err = errors.Errorf("pebble: keys must be added in order: %s, %s", w.err = errors.Errorf("pebble: keys must be added in order: %s, %s",
@@ -1149,7 +1259,7 @@ func (w *Writer) encodeRangeKeySpan(span keyspan.Span) {
func (w *Writer) addRangeKey(key InternalKey, value []byte) error { func (w *Writer) addRangeKey(key InternalKey, value []byte) error {
if !w.disableKeyOrderChecks && w.rangeKeyBlock.nEntries > 0 { if !w.disableKeyOrderChecks && w.rangeKeyBlock.nEntries > 0 {
prevStartKey := base.DecodeInternalKey(w.rangeKeyBlock.curKey) prevStartKey := w.rangeKeyBlock.getCurKey()
prevEndKey, _, ok := rangekey.DecodeEndKey(prevStartKey.Kind(), w.rangeKeyBlock.curValue) prevEndKey, _, ok := rangekey.DecodeEndKey(prevStartKey.Kind(), w.rangeKeyBlock.curValue)
if !ok { if !ok {
// We panic here as we should have previously decoded and validated this // We panic here as we should have previously decoded and validated this
@@ -1295,7 +1405,7 @@ func (w *Writer) flush(key InternalKey) error {
// dataBlockBuf is added back to a sync.Pool. In this particular case, the // dataBlockBuf is added back to a sync.Pool. In this particular case, the
// byte slice which supports "sep" will eventually be copied when "sep" is // byte slice which supports "sep" will eventually be copied when "sep" is
// added to the index block. // added to the index block.
prevKey := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey) prevKey := w.dataBlockBuf.dataBlock.getCurKey()
sep := w.indexEntrySep(prevKey, key, w.dataBlockBuf) sep := w.indexEntrySep(prevKey, key, w.dataBlockBuf)
// We determine that we should flush an index block from the Writer client // We determine that we should flush an index block from the Writer client
// goroutine, but we actually finish the index block from the writeQueue. // goroutine, but we actually finish the index block from the writeQueue.
@@ -1585,7 +1695,7 @@ func (w *Writer) finishIndexBlock(indexBuf *indexBlockBuf, props []byte) error {
nEntries: indexBuf.block.nEntries, properties: props, nEntries: indexBuf.block.nEntries, properties: props,
} }
w.indexSepAlloc, part.sep = cloneKeyWithBuf( w.indexSepAlloc, part.sep = cloneKeyWithBuf(
base.DecodeInternalKey(indexBuf.block.curKey), w.indexSepAlloc, indexBuf.block.getCurKey(), w.indexSepAlloc,
) )
bk := indexBuf.finish() bk := indexBuf.finish()
if len(w.indexBlockAlloc) < len(bk) { if len(w.indexBlockAlloc) < len(bk) {
@@ -1729,12 +1839,20 @@ func (w *Writer) assertFormatCompatibility() error {
) )
} }
// PebbleDBv3: value blocks.
if (w.props.NumValueBlocks > 0 || w.props.NumValuesInValueBlocks > 0 || if (w.props.NumValueBlocks > 0 || w.props.NumValuesInValueBlocks > 0 ||
w.props.ValueBlocksSize > 0) && w.tableFormat < TableFormatPebblev3 { w.props.ValueBlocksSize > 0) && w.tableFormat < TableFormatPebblev3 {
return errors.Newf( return errors.Newf(
"table format version %s is less than the minimum required version %s for value blocks", "table format version %s is less than the minimum required version %s for value blocks",
w.tableFormat, TableFormatPebblev3) w.tableFormat, TableFormatPebblev3)
} }
// PebbleDBv4: DELSIZED tombstones.
if w.props.NumSizedDeletions > 0 && w.tableFormat < TableFormatPebblev4 {
return errors.Newf(
"table format version %s is less than the minimum required version %s for sized deletion tombstones",
w.tableFormat, TableFormatPebblev4)
}
return nil return nil
} }
@@ -1781,7 +1899,7 @@ func (w *Writer) Close() (err error) {
// however, if a dataBlock is flushed, then we add a key to the new w.dataBlockBuf in the // however, if a dataBlock is flushed, then we add a key to the new w.dataBlockBuf in the
// addPoint function after the flush occurs. // addPoint function after the flush occurs.
if w.dataBlockBuf.dataBlock.nEntries >= 1 { if w.dataBlockBuf.dataBlock.nEntries >= 1 {
w.meta.SetLargestPointKey(base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey).Clone()) w.meta.SetLargestPointKey(w.dataBlockBuf.dataBlock.getCurKey().Clone())
} }
// Finish the last data block, or force an empty data block if there // Finish the last data block, or force an empty data block if there
@@ -1795,7 +1913,7 @@ func (w *Writer) Close() (err error) {
if err != nil { if err != nil {
return err return err
} }
prevKey := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey) prevKey := w.dataBlockBuf.dataBlock.getCurKey()
if err := w.addIndexEntrySync(prevKey, InternalKey{}, bhp, w.dataBlockBuf.tmp[:]); err != nil { if err := w.addIndexEntrySync(prevKey, InternalKey{}, bhp, w.dataBlockBuf.tmp[:]); err != nil {
return err return err
} }
@@ -1874,7 +1992,7 @@ func (w *Writer) Close() (err error) {
var rangeKeyBH BlockHandle var rangeKeyBH BlockHandle
if w.props.NumRangeKeys() > 0 { if w.props.NumRangeKeys() > 0 {
key := base.DecodeInternalKey(w.rangeKeyBlock.curKey) key := w.rangeKeyBlock.getCurKey()
kind := key.Kind() kind := key.Kind()
endKey, _, ok := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue) endKey, _, ok := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue)
if !ok { if !ok {
@@ -2063,7 +2181,7 @@ func (o PreviousPointKeyOpt) UnsafeKey() base.InternalKey {
if o.w.dataBlockBuf.dataBlock.nEntries >= 1 { if o.w.dataBlockBuf.dataBlock.nEntries >= 1 {
// o.w.dataBlockBuf.dataBlock.curKey is guaranteed to point to the last point key // o.w.dataBlockBuf.dataBlock.curKey is guaranteed to point to the last point key
// which was added to the Writer. // which was added to the Writer.
return base.DecodeInternalKey(o.w.dataBlockBuf.dataBlock.curKey) return o.w.dataBlockBuf.dataBlock.getCurKey()
} }
return base.InternalKey{} return base.InternalKey{}
} }
@@ -2092,6 +2210,8 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
separator: o.Comparer.Separator, separator: o.Comparer.Separator,
successor: o.Comparer.Successor, successor: o.Comparer.Successor,
tableFormat: o.TableFormat, tableFormat: o.TableFormat,
isStrictObsolete: o.IsStrictObsolete,
writingToLowestLevel: o.WritingToLowestLevel,
cache: o.Cache, cache: o.Cache,
restartInterval: o.BlockRestartInterval, restartInterval: o.BlockRestartInterval,
checksumType: o.Checksum, checksumType: o.Checksum,
@@ -2158,14 +2278,14 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
} }
} }
w.props.ColumnFamilyID = math.MaxInt32
w.props.ComparerName = o.Comparer.Name w.props.ComparerName = o.Comparer.Name
w.props.CompressionName = o.Compression.String() w.props.CompressionName = o.Compression.String()
w.props.MergerName = o.MergerName w.props.MergerName = o.MergerName
w.props.PropertyCollectorNames = "[]" w.props.PropertyCollectorNames = "[]"
w.props.ExternalFormatVersion = rocksDBExternalFormatVersion w.props.ExternalFormatVersion = rocksDBExternalFormatVersion
if len(o.TablePropertyCollectors) > 0 || len(o.BlockPropertyCollectors) > 0 { if len(o.TablePropertyCollectors) > 0 || len(o.BlockPropertyCollectors) > 0 ||
w.tableFormat >= TableFormatPebblev4 {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString("[") buf.WriteString("[")
if len(o.TablePropertyCollectors) > 0 { if len(o.TablePropertyCollectors) > 0 {
@@ -2178,16 +2298,22 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
buf.WriteString(w.propCollectors[i].Name()) buf.WriteString(w.propCollectors[i].Name())
} }
} }
if len(o.BlockPropertyCollectors) > 0 { numBlockPropertyCollectors := len(o.BlockPropertyCollectors)
if w.tableFormat >= TableFormatPebblev4 {
numBlockPropertyCollectors++
}
// shortID is a uint8, so we cannot exceed that number of block // shortID is a uint8, so we cannot exceed that number of block
// property collectors. // property collectors.
if len(o.BlockPropertyCollectors) > math.MaxUint8 { if numBlockPropertyCollectors > math.MaxUint8 {
w.err = errors.New("pebble: too many block property collectors") w.err = errors.New("pebble: too many block property collectors")
return w return w
} }
if numBlockPropertyCollectors > 0 {
w.blockPropCollectors = make([]BlockPropertyCollector, numBlockPropertyCollectors)
}
if len(o.BlockPropertyCollectors) > 0 {
// The shortID assigned to a collector is the same as its index in // The shortID assigned to a collector is the same as its index in
// this slice. // this slice.
w.blockPropCollectors = make([]BlockPropertyCollector, len(o.BlockPropertyCollectors))
for i := range o.BlockPropertyCollectors { for i := range o.BlockPropertyCollectors {
w.blockPropCollectors[i] = o.BlockPropertyCollectors[i]() w.blockPropCollectors[i] = o.BlockPropertyCollectors[i]()
if i > 0 || len(o.TablePropertyCollectors) > 0 { if i > 0 || len(o.TablePropertyCollectors) > 0 {
@@ -2196,6 +2322,13 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write
buf.WriteString(w.blockPropCollectors[i].Name()) buf.WriteString(w.blockPropCollectors[i].Name())
} }
} }
if w.tableFormat >= TableFormatPebblev4 {
if numBlockPropertyCollectors > 1 || len(o.TablePropertyCollectors) > 0 {
buf.WriteString(",")
}
w.blockPropCollectors[numBlockPropertyCollectors-1] = &w.obsoleteCollector
buf.WriteString(w.obsoleteCollector.Name())
}
buf.WriteString("]") buf.WriteString("]")
w.props.PropertyCollectorNames = buf.String() w.props.PropertyCollectorNames = buf.String()
} }
@@ -2229,3 +2362,85 @@ func init() {
} }
private.SSTableInternalProperties = internalGetProperties private.SSTableInternalProperties = internalGetProperties
} }
type obsoleteKeyBlockPropertyCollector struct {
blockIsNonObsolete bool
indexIsNonObsolete bool
tableIsNonObsolete bool
}
func encodeNonObsolete(isNonObsolete bool, buf []byte) []byte {
if isNonObsolete {
return buf
}
return append(buf, 't')
}
func (o *obsoleteKeyBlockPropertyCollector) Name() string {
return "obsolete-key"
}
func (o *obsoleteKeyBlockPropertyCollector) Add(key InternalKey, value []byte) error {
// Ignore.
return nil
}
func (o *obsoleteKeyBlockPropertyCollector) AddPoint(isObsolete bool) {
o.blockIsNonObsolete = o.blockIsNonObsolete || !isObsolete
}
func (o *obsoleteKeyBlockPropertyCollector) FinishDataBlock(buf []byte) ([]byte, error) {
o.tableIsNonObsolete = o.tableIsNonObsolete || o.blockIsNonObsolete
return encodeNonObsolete(o.blockIsNonObsolete, buf), nil
}
func (o *obsoleteKeyBlockPropertyCollector) AddPrevDataBlockToIndexBlock() {
o.indexIsNonObsolete = o.indexIsNonObsolete || o.blockIsNonObsolete
o.blockIsNonObsolete = false
}
func (o *obsoleteKeyBlockPropertyCollector) FinishIndexBlock(buf []byte) ([]byte, error) {
indexIsNonObsolete := o.indexIsNonObsolete
o.indexIsNonObsolete = false
return encodeNonObsolete(indexIsNonObsolete, buf), nil
}
func (o *obsoleteKeyBlockPropertyCollector) FinishTable(buf []byte) ([]byte, error) {
return encodeNonObsolete(o.tableIsNonObsolete, buf), nil
}
func (o *obsoleteKeyBlockPropertyCollector) UpdateKeySuffixes(
oldProp []byte, oldSuffix, newSuffix []byte,
) error {
_, err := propToIsObsolete(oldProp)
if err != nil {
return err
}
// Suffix rewriting currently loses the obsolete bit.
o.blockIsNonObsolete = true
return nil
}
// NB: obsoleteKeyBlockPropertyFilter is stateless. This aspect of the filter
// is used in table_cache.go for in-place modification of a filters slice.
type obsoleteKeyBlockPropertyFilter struct{}
func (o obsoleteKeyBlockPropertyFilter) Name() string {
return "obsolete-key"
}
// Intersects returns true if the set represented by prop intersects with
// the set in the filter.
func (o obsoleteKeyBlockPropertyFilter) Intersects(prop []byte) (bool, error) {
return propToIsObsolete(prop)
}
func propToIsObsolete(prop []byte) (bool, error) {
if len(prop) == 0 {
return true, nil
}
if len(prop) > 1 || prop[0] != 't' {
return false, errors.Errorf("unexpected property %x", prop)
}
return false, nil
}

View File

@@ -9,6 +9,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"math"
"runtime/debug" "runtime/debug"
"runtime/pprof" "runtime/pprof"
"sync" "sync"
@@ -21,6 +22,7 @@ import (
"github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/internal/private" "github.com/cockroachdb/pebble/internal/private"
"github.com/cockroachdb/pebble/internal/rangekey"
"github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/objstorage"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
"github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/pebble/sstable"
@@ -138,7 +140,7 @@ func (c *tableCacheContainer) newIters(
} }
func (c *tableCacheContainer) newRangeKeyIter( func (c *tableCacheContainer) newRangeKeyIter(
file *manifest.FileMetadata, opts *keyspan.SpanIterOptions, file *manifest.FileMetadata, opts keyspan.SpanIterOptions,
) (keyspan.FragmentIterator, error) { ) (keyspan.FragmentIterator, error) {
return c.tableCache.getShard(file.FileBacking.DiskFileNum).newRangeKeyIter(file, opts, &c.dbOpts) return c.tableCache.getShard(file.FileBacking.DiskFileNum).newRangeKeyIter(file, opts, &c.dbOpts)
} }
@@ -168,6 +170,32 @@ func (c *tableCacheContainer) metrics() (CacheMetrics, FilterMetrics) {
return m, f return m, f
} }
func (c *tableCacheContainer) estimateSize(
meta *fileMetadata, lower, upper []byte,
) (size uint64, err error) {
if meta.Virtual {
err = c.withVirtualReader(
meta.VirtualMeta(),
func(r sstable.VirtualReader) (err error) {
size, err = r.EstimateDiskUsage(lower, upper)
return err
},
)
} else {
err = c.withReader(
meta.PhysicalMeta(),
func(r *sstable.Reader) (err error) {
size, err = r.EstimateDiskUsage(lower, upper)
return err
},
)
}
if err != nil {
return 0, err
}
return size, nil
}
func (c *tableCacheContainer) withReader(meta physicalMeta, fn func(*sstable.Reader) error) error { func (c *tableCacheContainer) withReader(meta physicalMeta, fn func(*sstable.Reader) error) error {
s := c.tableCache.getShard(meta.FileBacking.DiskFileNum) s := c.tableCache.getShard(meta.FileBacking.DiskFileNum)
v := s.findNode(meta.FileMetadata, &c.dbOpts) v := s.findNode(meta.FileMetadata, &c.dbOpts)
@@ -373,6 +401,26 @@ func (c *tableCacheShard) newIters(
return nil, nil, v.err return nil, nil, v.err
} }
hideObsoletePoints := false
if opts != nil {
// This code is appending (at most one filter) in-place to
// opts.PointKeyFilters even though the slice is shared for iterators in
// the same iterator tree. This is acceptable since all the following
// properties are true:
// - The iterator tree is single threaded, so the shared backing for the
// slice is being mutated in a single threaded manner.
// - Each shallow copy of the slice has its own notion of length.
// - The appended element is always the obsoleteKeyBlockPropertyFilter
// struct, which is stateless, so overwriting that struct when creating
// one sstable iterator is harmless to other sstable iterators that are
// relying on that struct.
//
// An alternative would be to have different slices for different sstable
// iterators, but that requires more work to avoid allocations.
hideObsoletePoints, opts.PointKeyFilters =
v.reader.TryAddBlockPropertyFilterForHideObsoletePoints(
opts.snapshotForHideObsoletePoints, file.LargestSeqNum, opts.PointKeyFilters)
}
ok := true ok := true
var filterer *sstable.BlockPropertiesFilterer var filterer *sstable.BlockPropertiesFilterer
var err error var err error
@@ -387,14 +435,7 @@ func (c *tableCacheShard) newIters(
type iterCreator interface { type iterCreator interface {
NewRawRangeDelIter() (keyspan.FragmentIterator, error) NewRawRangeDelIter() (keyspan.FragmentIterator, error)
NewIterWithBlockPropertyFiltersAndContext( NewIterWithBlockPropertyFiltersAndContextEtc(ctx context.Context, lower, upper []byte, filterer *sstable.BlockPropertiesFilterer, hideObsoletePoints, useFilterBlock bool, stats *base.InternalIteratorStats, rp sstable.ReaderProvider) (sstable.Iterator, error)
ctx context.Context,
lower, upper []byte,
filterer *sstable.BlockPropertiesFilterer,
useFilterBlock bool,
stats *base.InternalIteratorStats,
rp sstable.ReaderProvider,
) (sstable.Iterator, error)
NewCompactionIter( NewCompactionIter(
bytesIterated *uint64, bytesIterated *uint64,
rp sstable.ReaderProvider, rp sstable.ReaderProvider,
@@ -410,11 +451,38 @@ func (c *tableCacheShard) newIters(
ic = &virtualReader ic = &virtualReader
} }
provider := dbOpts.objProvider
// Check if this file is a foreign file.
objMeta, err := provider.Lookup(fileTypeTable, file.FileBacking.DiskFileNum)
if err != nil {
return nil, nil, err
}
// NB: range-del iterator does not maintain a reference to the table, nor // NB: range-del iterator does not maintain a reference to the table, nor
// does it need to read from it after creation. // does it need to read from it after creation.
var rangeDelIter keyspan.FragmentIterator var rangeDelIter keyspan.FragmentIterator
if provider.IsForeign(objMeta) {
if opts == nil {
panic("unexpected nil opts when reading foreign file")
}
if !file.Virtual {
// Foreign sstables must be virtual by definition.
panic(fmt.Sprintf("sstable is foreign but not virtual: %s", file.FileNum))
}
switch manifest.LevelToInt(opts.level) {
case 5:
rangeDelIter, err = ic.NewRawRangeDelIter() rangeDelIter, err = ic.NewRawRangeDelIter()
case 6:
// Let rangeDelIter remain nil. We don't need to return rangedels from
// this file as they will not apply to any other files. For the purpose
// of collapsing rangedels within this file, we create another rangeDelIter
// below for use with the interleaving iter.
default:
panic(fmt.Sprintf("unexpected level for foreign sstable: %d", manifest.LevelToInt(opts.level)))
}
} else {
rangeDelIter, err = ic.NewRawRangeDelIter()
}
if err != nil { if err != nil {
c.unrefValue(v) c.unrefValue(v)
return nil, nil, err return nil, nil, err
@@ -454,11 +522,9 @@ func (c *tableCacheShard) newIters(
if internalOpts.bytesIterated != nil { if internalOpts.bytesIterated != nil {
iter, err = ic.NewCompactionIter(internalOpts.bytesIterated, rp) iter, err = ic.NewCompactionIter(internalOpts.bytesIterated, rp)
} else { } else {
iter, err = ic.NewIterWithBlockPropertyFiltersAndContext( iter, err = ic.NewIterWithBlockPropertyFiltersAndContextEtc(
ctx, ctx, opts.GetLowerBound(), opts.GetUpperBound(), filterer, hideObsoletePoints, useFilter,
opts.GetLowerBound(), opts.GetUpperBound(), internalOpts.stats, rp)
filterer, useFilter, internalOpts.stats, rp,
)
} }
if err != nil { if err != nil {
if rangeDelIter != nil { if rangeDelIter != nil {
@@ -470,6 +536,35 @@ func (c *tableCacheShard) newIters(
// NB: v.closeHook takes responsibility for calling unrefValue(v) here. Take // NB: v.closeHook takes responsibility for calling unrefValue(v) here. Take
// care to avoid introducing an allocation here by adding a closure. // care to avoid introducing an allocation here by adding a closure.
iter.SetCloseHook(v.closeHook) iter.SetCloseHook(v.closeHook)
if provider.IsForeign(objMeta) {
// NB: IsForeign() guarantees IsShared, so opts must not be nil as we've
// already panicked on the nil case above.
pointKeySeqNum := base.SeqNumForLevel(manifest.LevelToInt(opts.level))
pcIter := pointCollapsingIterator{
comparer: dbOpts.opts.Comparer,
merge: dbOpts.opts.Merge,
seqNum: math.MaxUint64,
elideRangeDeletes: true,
fixedSeqNum: pointKeySeqNum,
}
// Open a second rangedel iter. This is solely for the interleaving iter to
// be able to efficiently delete covered range deletes. We don't need to fix
// the sequence number in this iter, as these range deletes will not be
// exposed to anything other than the interleaving iter and
// pointCollapsingIter.
rangeDelIter, err := v.reader.NewRawRangeDelIter()
if err != nil {
c.unrefValue(v)
return nil, nil, err
}
if rangeDelIter == nil {
rangeDelIter = emptyKeyspanIter
}
pcIter.iter.Init(dbOpts.opts.Comparer, iter, rangeDelIter, nil /* mask */, opts.LowerBound, opts.UpperBound)
pcSSTIter := pcSSTIterPool.Get().(*pointCollapsingSSTIterator)
*pcSSTIter = pointCollapsingSSTIterator{pointCollapsingIterator: pcIter, childIter: iter}
iter = pcSSTIter
}
c.iterCount.Add(1) c.iterCount.Add(1)
dbOpts.iterCount.Add(1) dbOpts.iterCount.Add(1)
@@ -482,7 +577,7 @@ func (c *tableCacheShard) newIters(
} }
func (c *tableCacheShard) newRangeKeyIter( func (c *tableCacheShard) newRangeKeyIter(
file *manifest.FileMetadata, opts *keyspan.SpanIterOptions, dbOpts *tableCacheOpts, file *manifest.FileMetadata, opts keyspan.SpanIterOptions, dbOpts *tableCacheOpts,
) (keyspan.FragmentIterator, error) { ) (keyspan.FragmentIterator, error) {
// Calling findNode gives us the responsibility of decrementing v's // Calling findNode gives us the responsibility of decrementing v's
// refCount. If opening the underlying table resulted in error, then we // refCount. If opening the underlying table resulted in error, then we
@@ -501,7 +596,7 @@ func (c *tableCacheShard) newRangeKeyIter(
// file's range key blocks may surface deleted range keys below. This is // file's range key blocks may surface deleted range keys below. This is
// done here, rather than deferring to the block-property collector in order // done here, rather than deferring to the block-property collector in order
// to maintain parity with point keys and the treatment of RANGEDELs. // to maintain parity with point keys and the treatment of RANGEDELs.
if opts != nil && v.reader.Properties.NumRangeKeyDels == 0 { if v.reader.Properties.NumRangeKeyDels == 0 {
ok, _, err = c.checkAndIntersectFilters(v, nil, opts.RangeKeyFilters, nil) ok, _, err = c.checkAndIntersectFilters(v, nil, opts.RangeKeyFilters, nil)
} }
if err != nil { if err != nil {
@@ -539,6 +634,29 @@ func (c *tableCacheShard) newRangeKeyIter(
return emptyKeyspanIter, nil return emptyKeyspanIter, nil
} }
objMeta, err := dbOpts.objProvider.Lookup(fileTypeTable, file.FileBacking.DiskFileNum)
if err != nil {
return nil, err
}
if dbOpts.objProvider.IsForeign(objMeta) {
if opts.Level == 0 {
panic("unexpected zero level when reading foreign file")
}
transform := &rangekey.ForeignSSTTransformer{
Comparer: dbOpts.opts.Comparer,
Level: manifest.LevelToInt(opts.Level),
}
if iter == nil {
iter = emptyKeyspanIter
}
transformIter := &keyspan.TransformerIter{
FragmentIterator: iter,
Transformer: transform,
Compare: dbOpts.opts.Comparer.Compare,
}
return transformIter, nil
}
return iter, nil return iter, nil
} }
@@ -986,9 +1104,16 @@ type loadInfo struct {
func (v *tableCacheValue) load(loadInfo loadInfo, c *tableCacheShard, dbOpts *tableCacheOpts) { func (v *tableCacheValue) load(loadInfo loadInfo, c *tableCacheShard, dbOpts *tableCacheOpts) {
// Try opening the file first. // Try opening the file first.
var f objstorage.Readable var f objstorage.Readable
f, v.err = dbOpts.objProvider.OpenForReading( var err error
f, err = dbOpts.objProvider.OpenForReading(
context.TODO(), fileTypeTable, loadInfo.backingFileNum, objstorage.OpenOptions{MustExist: true}, context.TODO(), fileTypeTable, loadInfo.backingFileNum, objstorage.OpenOptions{MustExist: true},
) )
if err != nil {
v.err = errors.Wrap(
err,
fmt.Sprintf("pebble: could not open backing file %s", errors.Safe(loadInfo.backingFileNum.FileNum())),
)
}
if v.err == nil { if v.err == nil {
cacheOpts := private.SSTableCacheOpts(dbOpts.cacheID, loadInfo.backingFileNum).(sstable.ReaderOption) cacheOpts := private.SSTableCacheOpts(dbOpts.cacheID, loadInfo.backingFileNum).(sstable.ReaderOption)
v.reader, v.err = sstable.NewReader(f, dbOpts.opts, cacheOpts, dbOpts.filterMetrics) v.reader, v.err = sstable.NewReader(f, dbOpts.opts, cacheOpts, dbOpts.filterMetrics)

View File

@@ -126,7 +126,7 @@ func (d *DB) collectTableStats() bool {
maybeCompact := false maybeCompact := false
for _, c := range collected { for _, c := range collected {
c.fileMetadata.Stats = c.TableStats c.fileMetadata.Stats = c.TableStats
maybeCompact = maybeCompact || c.TableStats.RangeDeletionsBytesEstimate > 0 maybeCompact = maybeCompact || fileCompensation(c.fileMetadata) > 0
c.fileMetadata.StatsMarkValid() c.fileMetadata.StatsMarkValid()
} }
d.mu.tableStats.cond.Broadcast() d.mu.tableStats.cond.Broadcast()
@@ -186,6 +186,11 @@ func (d *DB) loadNewFileStats(
continue continue
} }
if nf.Meta.Virtual {
// cannot load virtual table stats
continue
}
stats, newHints, err := d.loadTableStats( stats, newHints, err := d.loadTableStats(
rs.current, nf.Level, rs.current, nf.Level,
nf.Meta.PhysicalMeta(), nf.Meta.PhysicalMeta(),
@@ -217,6 +222,12 @@ func (d *DB) scanReadStateTableStats(
for l, levelMetadata := range rs.current.Levels { for l, levelMetadata := range rs.current.Levels {
iter := levelMetadata.Iter() iter := levelMetadata.Iter()
for f := iter.First(); f != nil; f = iter.Next() { for f := iter.First(); f != nil; f = iter.Next() {
if f.Virtual {
// TODO(bananabrick): Support stats collection for virtual
// sstables.
continue
}
// NB: We're not holding d.mu which protects f.Stats, but only the // NB: We're not holding d.mu which protects f.Stats, but only the
// active stats collection job updates f.Stats for active files, // active stats collection job updates f.Stats for active files,
// and we ensure only one goroutine runs it at a time through // and we ensure only one goroutine runs it at a time through
@@ -225,6 +236,11 @@ func (d *DB) scanReadStateTableStats(
if f.StatsValid() { if f.StatsValid() {
continue continue
} }
// TODO(bilal): Remove this guard when table stats collection is
// implemented for virtual sstables.
if f.Virtual {
continue
}
// Limit how much work we do per read state. The older the read // Limit how much work we do per read state. The older the read
// state is, the higher the likelihood files are no longer being // state is, the higher the likelihood files are no longer being
@@ -304,12 +320,12 @@ func (d *DB) loadTablePointKeyStats(
// We could write the ranges of 'clusters' of point tombstones to // We could write the ranges of 'clusters' of point tombstones to
// a sstable property and call averageValueSizeBeneath for each of // a sstable property and call averageValueSizeBeneath for each of
// these narrower ranges to improve the estimate. // these narrower ranges to improve the estimate.
avgValSize, err := d.averagePhysicalValueSizeBeneath(v, level, meta) avgValLogicalSize, compressionRatio, err := d.estimateSizesBeneath(v, level, meta)
if err != nil { if err != nil {
return err return err
} }
stats.PointDeletionsBytesEstimate = stats.PointDeletionsBytesEstimate =
pointDeletionsBytesEstimate(meta.Size, &r.Properties, avgValSize) pointDeletionsBytesEstimate(meta.Size, &r.Properties, avgValLogicalSize, compressionRatio)
return nil return nil
} }
@@ -420,51 +436,60 @@ func (d *DB) loadTableRangeDelStats(
return compactionHints, err return compactionHints, err
} }
func (d *DB) averagePhysicalValueSizeBeneath( func (d *DB) estimateSizesBeneath(
v *version, level int, meta physicalMeta, v *version, level int, meta physicalMeta,
) (avgValueSize uint64, err error) { ) (avgValueLogicalSize, compressionRatio float64, err error) {
// Find all files in lower levels that overlap with meta, // Find all files in lower levels that overlap with meta,
// summing their value sizes and entry counts. // summing their value sizes and entry counts.
file := meta.FileMetadata
var fileSum, keySum, valSum, entryCount uint64 var fileSum, keySum, valSum, entryCount uint64
for l := level + 1; l < numLevels; l++ {
overlaps := v.Overlaps(l, d.cmp, meta.Smallest.UserKey, addPhysicalTableStats := func(r *sstable.Reader) (err error) {
meta.Largest.UserKey, meta.Largest.IsExclusiveSentinel())
iter := overlaps.Iter()
for file := iter.First(); file != nil; file = iter.Next() {
var err error
if file.Virtual {
err = d.tableCache.withVirtualReader(
file.VirtualMeta(),
func(v sstable.VirtualReader) (err error) {
fileSum += file.Size
entryCount += file.Stats.NumEntries
keySum += v.Properties.RawKeySize
valSum += v.Properties.RawValueSize
return nil
})
} else {
err = d.tableCache.withReader(
file.PhysicalMeta(),
func(r *sstable.Reader) (err error) {
fileSum += file.Size fileSum += file.Size
entryCount += r.Properties.NumEntries entryCount += r.Properties.NumEntries
keySum += r.Properties.RawKeySize keySum += r.Properties.RawKeySize
valSum += r.Properties.RawValueSize valSum += r.Properties.RawValueSize
return nil return nil
}) }
addVirtualTableStats := func(v sstable.VirtualReader) (err error) {
fileSum += file.Size
entryCount += file.Stats.NumEntries
keySum += v.Properties.RawKeySize
valSum += v.Properties.RawValueSize
return nil
} }
// Include the file itself. This is important because in some instances, the
// computed compression ratio is applied to the tombstones contained within
// `meta` itself. If there are no files beneath `meta` in the LSM, we would
// calculate a compression ratio of 0 which is not accurate for the file's
// own tombstones.
if err = d.tableCache.withReader(meta, addPhysicalTableStats); err != nil {
return 0, 0, err
}
for l := level + 1; l < numLevels; l++ {
overlaps := v.Overlaps(l, d.cmp, meta.Smallest.UserKey,
meta.Largest.UserKey, meta.Largest.IsExclusiveSentinel())
iter := overlaps.Iter()
for file = iter.First(); file != nil; file = iter.Next() {
var err error
if file.Virtual {
err = d.tableCache.withVirtualReader(file.VirtualMeta(), addVirtualTableStats)
} else {
err = d.tableCache.withReader(file.PhysicalMeta(), addPhysicalTableStats)
}
if err != nil { if err != nil {
return 0, err return 0, 0, err
} }
} }
} }
if entryCount == 0 { if entryCount == 0 {
return 0, nil return 0, 0, nil
} }
// RawKeySize and RawValueSize are uncompressed totals. Scale the value sum // RawKeySize and RawValueSize are uncompressed totals. We'll need to scale
// according to the data size to account for compression, index blocks and // the value sum according to the data size to account for compression,
// metadata overhead. Eg: // index blocks and metadata overhead. Eg:
// //
// Compression rate × Average uncompressed value size // Compression rate × Average uncompressed value size
// //
@@ -473,10 +498,14 @@ func (d *DB) averagePhysicalValueSizeBeneath(
// FileSize RawValueSize // FileSize RawValueSize
// ----------------------- × ------------ // ----------------------- × ------------
// RawKeySize+RawValueSize NumEntries // RawKeySize+RawValueSize NumEntries
//
// We return the average logical value size plus the compression ratio,
// leaving the scaling to the caller. This allows the caller to perform
// additional compression ratio scaling if necessary.
uncompressedSum := float64(keySum + valSum) uncompressedSum := float64(keySum + valSum)
compressionRatio := float64(fileSum) / uncompressedSum compressionRatio = float64(fileSum) / uncompressedSum
avgCompressedValueSize := (float64(valSum) / float64(entryCount)) * compressionRatio avgValueLogicalSize = (float64(valSum) / float64(entryCount))
return uint64(avgCompressedValueSize), nil return avgValueLogicalSize, compressionRatio, nil
} }
func (d *DB) estimateReclaimedSizeBeneath( func (d *DB) estimateReclaimedSizeBeneath(
@@ -593,12 +622,12 @@ func maybeSetStatsFromProperties(meta physicalMeta, props *sstable.Properties) b
return false return false
} }
// If a table is more than 10% point deletions, don't calculate the // If a table is more than 10% point deletions without user-provided size
// PointDeletionsBytesEstimate statistic using our limited knowledge. The // estimates, don't calculate the PointDeletionsBytesEstimate statistic
// table stats collector can populate the stats and calculate an average // using our limited knowledge. The table stats collector can populate the
// of value size of all the tables beneath the table in the LSM, which // stats and calculate an average of value size of all the tables beneath
// will be more accurate. // the table in the LSM, which will be more accurate.
if props.NumDeletions > props.NumEntries/10 { if unsizedDels := (props.NumDeletions - props.NumSizedDeletions); unsizedDels > props.NumEntries/10 {
return false return false
} }
@@ -608,8 +637,8 @@ func maybeSetStatsFromProperties(meta physicalMeta, props *sstable.Properties) b
// doesn't require any additional IO and since the number of point // doesn't require any additional IO and since the number of point
// deletions in the file is low, the error introduced by this crude // deletions in the file is low, the error introduced by this crude
// estimate is expected to be small. // estimate is expected to be small.
avgValSize := estimateValuePhysicalSize(meta.Size, props) avgValSize, compressionRatio := estimatePhysicalSizes(meta.Size, props)
pointEstimate = pointDeletionsBytesEstimate(meta.Size, props, avgValSize) pointEstimate = pointDeletionsBytesEstimate(meta.Size, props, avgValSize, compressionRatio)
} }
meta.Stats.NumEntries = props.NumEntries meta.Stats.NumEntries = props.NumEntries
@@ -623,7 +652,7 @@ func maybeSetStatsFromProperties(meta physicalMeta, props *sstable.Properties) b
} }
func pointDeletionsBytesEstimate( func pointDeletionsBytesEstimate(
fileSize uint64, props *sstable.Properties, avgValPhysicalSize uint64, fileSize uint64, props *sstable.Properties, avgValLogicalSize, compressionRatio float64,
) (estimate uint64) { ) (estimate uint64) {
if props.NumEntries == 0 { if props.NumEntries == 0 {
return 0 return 0
@@ -639,43 +668,79 @@ func pointDeletionsBytesEstimate(
// If there are covered key(s), we also get to drop key and value bytes for // If there are covered key(s), we also get to drop key and value bytes for
// each covered key. // each covered key.
// //
// We estimate assuming that each point tombstone on average covers 1 key. // Some point tombstones (DELSIZEDs) carry a user-provided estimate of the
// uncompressed size of entries that will be elided by fully compacting the
// tombstone. For these tombstones, there's no guesswork—we use the
// RawPointTombstoneValueSizeHint property which is the sum of all these
// tombstones' encoded values.
//
// For un-sized point tombstones (DELs), we estimate assuming that each
// point tombstone on average covers 1 key and using average vaue sizes.
// This is almost certainly an overestimate, but that's probably okay // This is almost certainly an overestimate, but that's probably okay
// because point tombstones can slow range iterations even when they don't // because point tombstones can slow range iterations even when they don't
// cover a key. It may be beneficial in the future to more accurately // cover a key.
// estimate which tombstones cover keys and which do not. //
// TODO(jackson): This logic doesn't directly incorporate fixed per-key
// overhead (8-byte trailer, plus at least 1 byte encoding the length of the
// key and 1 byte encoding the length of the value). This overhead is
// indirectly incorporated through the compression ratios, but that results
// in the overhead being smeared per key-byte and value-byte, rather than
// per-entry. This per-key fixed overhead can be nontrivial, especially for
// dense swaths of point tombstones. Give some thought as to whether we
// should directly include fixed per-key overhead in the calculations.
// Calculate the contribution of the key sizes: 1x for the point tombstone // Below, we calculate the tombstone contributions and the shadowed keys'
// itself and 1x for the key that's expected to exist lower in the LSM. // contributions separately.
var keysLogicalSize uint64 var tombstonesLogicalSize float64
var shadowedLogicalSize float64
// 1. Calculate the contribution of the tombstone keys themselves.
if props.RawPointTombstoneKeySize > 0 { if props.RawPointTombstoneKeySize > 0 {
// This table has a RawPointTombstoneKeySize property, so we know the tombstonesLogicalSize += float64(props.RawPointTombstoneKeySize)
// exact size of the logical, uncompressed bytes of the point tombstone
// keys.
keysLogicalSize = 2 * props.RawPointTombstoneKeySize
} else { } else {
// This sstable predates the existence of the RawPointTombstoneKeySize // This sstable predates the existence of the RawPointTombstoneKeySize
// property. We can use the average key size within the file itself and // property. We can use the average key size within the file itself and
// the count of point deletions to estimate the size. // the count of point deletions to estimate the size.
keysLogicalSize = 2 * numPointDels * props.RawKeySize / props.NumEntries tombstonesLogicalSize += float64(numPointDels * props.RawKeySize / props.NumEntries)
} }
// Scale the logical key size by the logical:physical ratio of the file to
// 2. Calculate the contribution of the keys shadowed by tombstones.
//
// 2a. First account for keys shadowed by DELSIZED tombstones. THE DELSIZED
// tombstones encode the size of both the key and value of the shadowed KV
// entries. These sizes are aggregated into a sstable property.
shadowedLogicalSize += float64(props.RawPointTombstoneValueSize)
// 2b. Calculate the contribution of the KV entries shadowed by ordinary DEL
// keys.
numUnsizedDels := numPointDels - props.NumSizedDeletions
{
// The shadowed keys have the same exact user keys as the tombstones
// themselves, so we can use the `tombstonesLogicalSize` we computed
// earlier as an estimate. There's a complication that
// `tombstonesLogicalSize` may include DELSIZED keys we already
// accounted for.
shadowedLogicalSize += float64(tombstonesLogicalSize) / float64(numPointDels) * float64(numUnsizedDels)
// Calculate the contribution of the deleted values. The caller has
// already computed an average logical size (possibly computed across
// many sstables).
shadowedLogicalSize += float64(numUnsizedDels) * avgValLogicalSize
}
// Scale both tombstone and shadowed totals by logical:physical ratios to
// account for compression, metadata overhead, etc. // account for compression, metadata overhead, etc.
// //
// Physical FileSize // Physical FileSize
// ----------- = ----------------------- // ----------- = -----------------------
// Logical RawKeySize+RawValueSize // Logical RawKeySize+RawValueSize
// //
estimate += (keysLogicalSize * fileSize) / (props.RawKeySize + props.RawValueSize) return uint64((tombstonesLogicalSize + shadowedLogicalSize) * compressionRatio)
// Calculate the contribution of the deleted values. The caller has already
// computed an average physical size (possibly comptued across many
// sstables), so there's no need to scale it.
estimate += numPointDels * avgValPhysicalSize
return estimate
} }
func estimateValuePhysicalSize(fileSize uint64, props *sstable.Properties) (avgValSize uint64) { func estimatePhysicalSizes(
fileSize uint64, props *sstable.Properties,
) (avgValLogicalSize, compressionRatio float64) {
// RawKeySize and RawValueSize are uncompressed totals. Scale according to // RawKeySize and RawValueSize are uncompressed totals. Scale according to
// the data size to account for compression, index blocks and metadata // the data size to account for compression, index blocks and metadata
// overhead. Eg: // overhead. Eg:
@@ -689,9 +754,9 @@ func estimateValuePhysicalSize(fileSize uint64, props *sstable.Properties) (avgV
// RawKeySize+RawValueSize NumEntries // RawKeySize+RawValueSize NumEntries
// //
uncompressedSum := props.RawKeySize + props.RawValueSize uncompressedSum := props.RawKeySize + props.RawValueSize
compressionRatio := float64(fileSize) / float64(uncompressedSum) compressionRatio = float64(fileSize) / float64(uncompressedSum)
avgCompressedValueSize := (float64(props.RawValueSize) / float64(props.NumEntries)) * compressionRatio avgValLogicalSize = (float64(props.RawValueSize) / float64(props.NumEntries))
return uint64(avgCompressedValueSize) return avgValLogicalSize, compressionRatio
} }
// newCombinedDeletionKeyspanIter returns a keyspan.FragmentIterator that // newCombinedDeletionKeyspanIter returns a keyspan.FragmentIterator that

View File

@@ -293,8 +293,8 @@ func (vs *versionSet) load(
} }
vs.markFileNumUsed(vs.minUnflushedLogNum) vs.markFileNumUsed(vs.minUnflushedLogNum)
// Populate the fileBackingMap since we have finished version // Populate the fileBackingMap and the FileBacking for virtual sstables since
// edit accumulation. // we have finished version edit accumulation.
for _, s := range bve.AddedFileBacking { for _, s := range bve.AddedFileBacking {
vs.fileBackingMap[s.DiskFileNum] = s vs.fileBackingMap[s.DiskFileNum] = s
} }
@@ -490,6 +490,9 @@ func (vs *versionSet) logAndApply(
defer vs.mu.Lock() defer vs.mu.Lock()
var err error var err error
if vs.opts.FormatMajorVersion < ExperimentalFormatVirtualSSTables && len(ve.CreatedBackingTables) > 0 {
return errors.AssertionFailedf("MANIFEST cannot contain virtual sstable records due to format major version")
}
newVersion, zombies, err = manifest.AccumulateIncompleteAndApplySingleVE( newVersion, zombies, err = manifest.AccumulateIncompleteAndApplySingleVE(
ve, currentVersion, vs.cmp, vs.opts.Comparer.FormatKey, ve, currentVersion, vs.cmp, vs.opts.Comparer.FormatKey,
vs.opts.FlushSplitBytes, vs.opts.Experimental.ReadCompactionRate, vs.opts.FlushSplitBytes, vs.opts.Experimental.ReadCompactionRate,
@@ -516,7 +519,7 @@ func (vs *versionSet) logAndApply(
return errors.Wrap(err, "MANIFEST next record write failed") return errors.Wrap(err, "MANIFEST next record write failed")
} }
// NB: Any error from this point on is considered fatal as we don't now if // NB: Any error from this point on is considered fatal as we don't know if
// the MANIFEST write occurred or not. Trying to determine that is // the MANIFEST write occurred or not. Trying to determine that is
// fraught. Instead we rely on the standard recovery mechanism run when a // fraught. Instead we rely on the standard recovery mechanism run when a
// database is open. In particular, that mechanism generates a new MANIFEST // database is open. In particular, that mechanism generates a new MANIFEST
@@ -569,6 +572,14 @@ func (vs *versionSet) logAndApply(
for fileNum, size := range zombies { for fileNum, size := range zombies {
vs.zombieTables[fileNum] = size vs.zombieTables[fileNum] = size
} }
// Update the Deleted bools. We can't use the zombieTables struct for this
// as it works on FileBackings, not FileMetadatas.
for _, f := range ve.DeletedFiles {
f.Deleted = true
}
for i := range ve.NewFiles {
ve.NewFiles[i].Meta.Deleted = false
}
// Install the new version. // Install the new version.
vs.append(newVersion) vs.append(newVersion)
@@ -688,7 +699,6 @@ func (vs *versionSet) createManifest(
Level: level, Level: level,
Meta: meta, Meta: meta,
}) })
// TODO(bananabrick): Test snapshot changes.
if _, ok := dedup[meta.FileBacking.DiskFileNum]; meta.Virtual && !ok { if _, ok := dedup[meta.FileBacking.DiskFileNum]; meta.Virtual && !ok {
dedup[meta.FileBacking.DiskFileNum] = struct{}{} dedup[meta.FileBacking.DiskFileNum] = struct{}{}
snapshot.CreatedBackingTables = append( snapshot.CreatedBackingTables = append(

View File

@@ -228,6 +228,17 @@ func (fs *enospcFS) Open(name string, opts ...OpenOption) (File, error) {
return f, err return f, err
} }
func (fs *enospcFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
f, err := fs.inner.OpenReadWrite(name, opts...)
if f != nil {
f = &enospcFile{
fs: fs,
inner: f,
}
}
return f, err
}
func (fs *enospcFS) OpenDir(name string) (File, error) { func (fs *enospcFS) OpenDir(name string) (File, error) {
f, err := fs.inner.OpenDir(name) f, err := fs.inner.OpenDir(name)
if f != nil { if f != nil {
@@ -375,6 +386,20 @@ func (f *enospcFile) Write(p []byte) (n int, err error) {
return n, err return n, err
} }
func (f *enospcFile) WriteAt(p []byte, ofs int64) (n int, err error) {
gen := f.fs.waitUntilReady()
n, err = f.inner.WriteAt(p, ofs)
if err != nil && isENOSPC(err) {
f.fs.handleENOSPC(gen)
var n2 int
n2, err = f.inner.WriteAt(p[n:], ofs+int64(n))
n += n2
}
return n, err
}
func (f *enospcFile) Prefetch(offset, length int64) error { func (f *enospcFile) Prefetch(offset, length int64) error {
return f.inner.Prefetch(offset, length) return f.inner.Prefetch(offset, length)
} }

View File

@@ -219,6 +219,14 @@ func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) {
return n, err return n, err
} }
// Write implements the io.WriterAt interface.
func (d *diskHealthCheckingFile) WriteAt(p []byte, ofs int64) (n int, err error) {
d.timeDiskOp(OpTypeWrite, int64(len(p)), func() {
n, err = d.file.WriteAt(p, ofs)
})
return n, err
}
// Close implements the io.Closer interface. // Close implements the io.Closer interface.
func (d *diskHealthCheckingFile) Close() error { func (d *diskHealthCheckingFile) Close() error {
d.stopTicker() d.stopTicker()
@@ -680,6 +688,11 @@ func (d *diskHealthCheckingFS) Open(name string, opts ...OpenOption) (File, erro
return d.fs.Open(name, opts...) return d.fs.Open(name, opts...)
} }
// OpenReadWrite implements the FS interface.
func (d *diskHealthCheckingFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
return d.fs.Open(name, opts...)
}
// OpenDir implements the FS interface. // OpenDir implements the FS interface.
func (d *diskHealthCheckingFS) OpenDir(name string) (File, error) { func (d *diskHealthCheckingFS) OpenDir(name string) (File, error) {
f, err := d.fs.OpenDir(name) f, err := d.fs.OpenDir(name)

View File

@@ -46,6 +46,15 @@ func (fs *loggingFS) Open(name string, opts ...OpenOption) (File, error) {
return newLoggingFile(f, name, fs.logFn), nil return newLoggingFile(f, name, fs.logFn), nil
} }
func (fs *loggingFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
fs.logFn("open-read-write: %s", name)
f, err := fs.FS.OpenReadWrite(name, opts...)
if err != nil {
return nil, err
}
return newLoggingFile(f, name, fs.logFn), nil
}
func (fs *loggingFS) Link(oldname, newname string) error { func (fs *loggingFS) Link(oldname, newname string) error {
fs.logFn("link: %s -> %s", oldname, newname) fs.logFn("link: %s -> %s", oldname, newname)
return fs.FS.Link(oldname, newname) return fs.FS.Link(oldname, newname)
@@ -137,6 +146,11 @@ func (f *loggingFile) ReadAt(p []byte, offset int64) (int, error) {
return f.File.ReadAt(p, offset) return f.File.ReadAt(p, offset)
} }
func (f *loggingFile) WriteAt(p []byte, offset int64) (int, error) {
f.logFn("write-at(%d, %d): %s", offset, len(p), f.name)
return f.File.WriteAt(p, offset)
}
func (f *loggingFile) Prefetch(offset int64, length int64) error { func (f *loggingFile) Prefetch(offset int64, length int64) error {
f.logFn("prefetch(%d, %d): %s", offset, length, f.name) f.logFn("prefetch(%d, %d): %s", offset, length, f.name)
return f.File.Prefetch(offset, length) return f.File.Prefetch(offset, length)

View File

@@ -208,6 +208,7 @@ func (y *MemFS) Create(fullname string) (File, error) {
ret = &memFile{ ret = &memFile{
n: n, n: n,
fs: y, fs: y,
read: true,
write: true, write: true,
} }
} }
@@ -262,7 +263,7 @@ func (y *MemFS) Link(oldname, newname string) error {
}) })
} }
func (y *MemFS) open(fullname string) (File, error) { func (y *MemFS) open(fullname string, openForWrite bool) (File, error) {
var ret *memFile var ret *memFile
err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { err := y.walk(fullname, func(dir *memNode, frag string, final bool) error {
if final { if final {
@@ -278,6 +279,7 @@ func (y *MemFS) open(fullname string) (File, error) {
n: n, n: n,
fs: y, fs: y,
read: true, read: true,
write: openForWrite,
} }
} }
} }
@@ -299,12 +301,22 @@ func (y *MemFS) open(fullname string) (File, error) {
// Open implements FS.Open. // Open implements FS.Open.
func (y *MemFS) Open(fullname string, opts ...OpenOption) (File, error) { func (y *MemFS) Open(fullname string, opts ...OpenOption) (File, error) {
return y.open(fullname) return y.open(fullname, false /* openForWrite */)
}
// OpenReadWrite implements FS.OpenReadWrite.
func (y *MemFS) OpenReadWrite(fullname string, opts ...OpenOption) (File, error) {
f, err := y.open(fullname, true /* openForWrite */)
pathErr, ok := err.(*os.PathError)
if ok && pathErr.Err == oserror.ErrNotExist {
return y.Create(fullname)
}
return f, err
} }
// OpenDir implements FS.OpenDir. // OpenDir implements FS.OpenDir.
func (y *MemFS) OpenDir(fullname string) (File, error) { func (y *MemFS) OpenDir(fullname string) (File, error) {
return y.open(fullname) return y.open(fullname, false /* openForWrite */)
} }
// Remove implements FS.Remove. // Remove implements FS.Remove.
@@ -702,6 +714,29 @@ func (f *memFile) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
func (f *memFile) WriteAt(p []byte, ofs int64) (int, error) {
if !f.write {
return 0, errors.New("pebble/vfs: file was not created for writing")
}
if f.n.isDir {
return 0, errors.New("pebble/vfs: cannot write a directory")
}
f.n.mu.Lock()
defer f.n.mu.Unlock()
f.n.mu.modTime = time.Now()
for len(f.n.mu.data) < int(ofs)+len(p) {
f.n.mu.data = append(f.n.mu.data, 0)
}
n := copy(f.n.mu.data[int(ofs):int(ofs)+len(p)], p)
if n != len(p) {
panic("stuff")
}
return len(p), nil
}
func (f *memFile) Prefetch(offset int64, length int64) error { return nil } func (f *memFile) Prefetch(offset int64, length int64) error { return nil }
func (f *memFile) Preallocate(offset, length int64) error { return nil } func (f *memFile) Preallocate(offset, length int64) error { return nil }

View File

@@ -29,6 +29,8 @@ type File interface {
// method *is* allowed to modify the slice passed in, whether temporarily // method *is* allowed to modify the slice passed in, whether temporarily
// or permanently. Callers of Write() need to take this into account. // or permanently. Callers of Write() need to take this into account.
io.Writer io.Writer
// WriteAt() is only supported for files that were opened with FS.OpenReadWrite.
io.WriterAt
// Preallocate optionally preallocates storage for `length` at `offset` // Preallocate optionally preallocates storage for `length` at `offset`
// within the file. Implementations may choose to do nothing. // within the file. Implementations may choose to do nothing.
@@ -99,6 +101,10 @@ type FS interface {
// Open opens the named file for reading. openOptions provides // Open opens the named file for reading. openOptions provides
Open(name string, opts ...OpenOption) (File, error) Open(name string, opts ...OpenOption) (File, error)
// OpenReadWrite opens the named file for reading and writing. If the file
// does not exist, it is created.
OpenReadWrite(name string, opts ...OpenOption) (File, error)
// OpenDir opens the named directory for syncing. // OpenDir opens the named directory for syncing.
OpenDir(name string) (File, error) OpenDir(name string) (File, error)
@@ -232,6 +238,18 @@ func (defaultFS) Open(name string, opts ...OpenOption) (File, error) {
return file, nil return file, nil
} }
func (defaultFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
osFile, err := os.OpenFile(name, os.O_RDWR|syscall.O_CLOEXEC|os.O_CREATE, 0666)
if err != nil {
return nil, errors.WithStack(err)
}
file := wrapOSFile(osFile)
for _, opt := range opts {
opt.Apply(file)
}
return file, nil
}
func (defaultFS) Remove(name string) error { func (defaultFS) Remove(name string) error {
return errors.WithStack(os.Remove(name)) return errors.WithStack(os.Remove(name))
} }

View File

@@ -15,6 +15,7 @@
package buffer package buffer
import ( import (
"bytes"
origFmt "fmt" origFmt "fmt"
"unicode/utf8" "unicode/utf8"
"unsafe" "unsafe"
@@ -197,11 +198,16 @@ func (b *Buffer) endRedactable() {
if len(b.buf) == 0 { if len(b.buf) == 0 {
return return
} }
p, ok := b.tryGrowByReslice(len(m.EndS)) if bytes.HasSuffix(b.buf, m.StartBytes) {
// Special case: remove a trailing open marker and call it a day.
b.buf = b.buf[:len(b.buf)-m.StartLen]
} else {
p, ok := b.tryGrowByReslice(m.EndLen)
if !ok { if !ok {
p = b.grow(len(m.EndS)) p = b.grow(m.EndLen)
} }
copy(b.buf[p:], m.EndS) copy(b.buf[p:], m.EndS)
}
b.markerOpen = false b.markerOpen = false
} }
@@ -214,18 +220,23 @@ func (b *Buffer) startWrite() {
// endRedactable adds the closing redaction marker. // endRedactable adds the closing redaction marker.
func (b *Buffer) startRedactable() { func (b *Buffer) startRedactable() {
if bytes.HasSuffix(b.buf, m.EndBytes) {
// Special case: remove a trailing closing marker and call it a day.
b.buf = b.buf[:len(b.buf)-m.EndLen]
} else {
p, ok := b.tryGrowByReslice(len(m.StartS)) p, ok := b.tryGrowByReslice(len(m.StartS))
if !ok { if !ok {
p = b.grow(len(m.StartS)) p = b.grow(len(m.StartS))
} }
copy(b.buf[p:], m.StartS) copy(b.buf[p:], m.StartS)
}
b.markerOpen = true b.markerOpen = true
} }
// escapeToEnd escapes occurrences of redaction markers in // escapeToEnd escapes occurrences of redaction markers in
// b.buf[b.validUntil:] and advances b.validUntil until the end. // b.buf[b.validUntil:] and advances b.validUntil until the end.
func (b *Buffer) escapeToEnd(breakNewLines bool) { func (b *Buffer) escapeToEnd(breakNewLines bool) {
b.buf = escape.InternalEscapeBytes(b.buf, b.validUntil, breakNewLines, breakNewLines) b.buf = escape.InternalEscapeBytes(b.buf, b.validUntil, breakNewLines, false /* trim */)
b.validUntil = len(b.buf) b.validUntil = len(b.buf)
} }
@@ -313,6 +324,13 @@ func (b *Buffer) Grow(n int) {
b.buf = b.buf[:m] b.buf = b.buf[:m]
} }
// clone is used in tests.
func (b *Buffer) clone() *Buffer {
c := *b
c.buf = append([]byte(nil), b.buf...)
return &c
}
// makeSlice allocates a slice of size n. If the allocation fails, it panics // makeSlice allocates a slice of size n. If the allocation fails, it panics
// with ErrTooLarge. // with ErrTooLarge.
func makeSlice(n int) []byte { func makeSlice(n int) []byte {

View File

@@ -28,6 +28,9 @@ import (
// If breakNewLines is set, a closing redaction marker // If breakNewLines is set, a closing redaction marker
// is placed before sequences of one or more newline characters, // is placed before sequences of one or more newline characters,
// and an open redaction marker is placed afterwards. // and an open redaction marker is placed afterwards.
//
// If strip is set, final newlines and spaces are trimmed from the
// output.
func InternalEscapeBytes(b []byte, startLoc int, breakNewLines, strip bool) (res []byte) { func InternalEscapeBytes(b []byte, startLoc int, breakNewLines, strip bool) (res []byte) {
// Note: we use len(...RedactableS) and not len(...RedactableBytes) // Note: we use len(...RedactableS) and not len(...RedactableBytes)
// because the ...S variant is a compile-time constant so this // because the ...S variant is a compile-time constant so this
@@ -69,7 +72,12 @@ func InternalEscapeBytes(b []byte, startLoc int, breakNewLines, strip bool) (res
copied = true copied = true
} }
res = append(res, b[k:i]...) res = append(res, b[k:i]...)
// Either add an end marker, or elide a start marker immediately prior.
if bytes.HasSuffix(res, start) {
res = res[:len(res)-ls]
} else {
res = append(res, end...) res = append(res, end...)
}
// Advance to the last newline character. We want to forward // Advance to the last newline character. We want to forward
// them all in a single call to doWrite, for performance. // them all in a single call to doWrite, for performance.
lastNewLine := i lastNewLine := i

View File

@@ -20,8 +20,10 @@ import "regexp"
const ( const (
Start = '‹' Start = '‹'
StartS = string(Start) StartS = string(Start)
StartLen = len(StartS)
End = '›' End = '›'
EndS = string(End) EndS = string(End)
EndLen = len(EndS)
EscapeMark = '?' EscapeMark = '?'
EscapeMarkS = string(EscapeMark) EscapeMarkS = string(EscapeMark)
RedactedS = StartS + "×" + EndS RedactedS = StartS + "×" + EndS

View File

@@ -16,6 +16,10 @@ This package provides various compression algorithms.
# changelog # changelog
* Apr 16, 2023 - [v1.16.5](https://github.com/klauspost/compress/releases/tag/v1.16.5)
* zstd: readByte needs to use io.ReadFull by @jnoxon in https://github.com/klauspost/compress/pull/802
* gzip: Fix WriterTo after initial read https://github.com/klauspost/compress/pull/804
* Apr 5, 2023 - [v1.16.4](https://github.com/klauspost/compress/releases/tag/v1.16.4) * Apr 5, 2023 - [v1.16.4](https://github.com/klauspost/compress/releases/tag/v1.16.4)
* zstd: Improve zstd best efficiency by @greatroar and @klauspost in https://github.com/klauspost/compress/pull/784 * zstd: Improve zstd best efficiency by @greatroar and @klauspost in https://github.com/klauspost/compress/pull/784
* zstd: Respect WithAllLitEntropyCompression https://github.com/klauspost/compress/pull/792 * zstd: Respect WithAllLitEntropyCompression https://github.com/klauspost/compress/pull/792

25
vendor/github.com/klauspost/compress/SECURITY.md generated vendored Normal file
View File

@@ -0,0 +1,25 @@
# Security Policy
## Supported Versions
Security updates are applied only to the latest release.
## Vulnerability Definition
A security vulnerability is a bug that with certain input triggers a crash or an infinite loop. Most calls will have varying execution time and only in rare cases will slow operation be considered a security vulnerability.
Corrupted output generally is not considered a security vulnerability, unless independent operations are able to affect each other. Note that not all functionality is re-entrant and safe to use concurrently.
Out-of-memory crashes only applies if the en/decoder uses an abnormal amount of memory, with appropriate options applied, to limit maximum window size, concurrency, etc. However, if you are in doubt you are welcome to file a security issue.
It is assumed that all callers are trusted, meaning internal data exposed through reflection or inspection of returned data structures is not considered a vulnerability.
Vulnerabilities resulting from compiler/assembler errors should be reported upstream. Depending on the severity this package may or may not implement a workaround.
## Reporting a Vulnerability
If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.
Please disclose it at [security advisory](https://github.com/klaupost/compress/security/advisories/new). If possible please provide a minimal reproducer. If the issue only applies to a single platform, it would be helpful to provide access to that.
This project is maintained by a team of volunteers on a reasonable-effort basis. As such, vulnerabilities will be disclosed in a best effort base.

View File

@@ -13,14 +13,6 @@ type bitWriter struct {
out []byte out []byte
} }
// bitMask16 is bitmasks. Has extra to avoid bounds check.
var bitMask16 = [32]uint16{
0, 1, 3, 7, 0xF, 0x1F,
0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF,
0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF} /* up to 16 bits */
// addBits16Clean will add up to 16 bits. value may not contain more set bits than indicated. // addBits16Clean will add up to 16 bits. value may not contain more set bits than indicated.
// It will not check if there is space for them, so the caller must ensure that it has flushed recently. // It will not check if there is space for them, so the caller must ensure that it has flushed recently.
func (b *bitWriter) addBits16Clean(value uint16, bits uint8) { func (b *bitWriter) addBits16Clean(value uint16, bits uint8) {

View File

@@ -253,7 +253,7 @@ func (d *Decoder) decompress1X8Bit(dst, src []byte) ([]byte, error) {
switch d.actualTableLog { switch d.actualTableLog {
case 8: case 8:
const shift = 8 - 8 const shift = 0
for br.off >= 4 { for br.off >= 4 {
br.fillFast() br.fillFast()
v := dt[uint8(br.value>>(56+shift))] v := dt[uint8(br.value>>(56+shift))]

View File

@@ -87,18 +87,6 @@ func emitCopy(dst []byte, offset, length int) int {
return i + 2 return i + 2
} }
// extendMatch returns the largest k such that k <= len(src) and that
// src[i:i+k-j] and src[j:k] have the same contents.
//
// It assumes that:
//
// 0 <= i && i < j && j <= len(src)
func extendMatch(src []byte, i, j int) int {
for ; j < len(src) && src[i] == src[j]; i, j = i+1, j+1 {
}
return j
}
func hash(u, shift uint32) uint32 { func hash(u, shift uint32) uint32 {
return (u * 0x1e35a7bd) >> shift return (u * 0x1e35a7bd) >> shift
} }

View File

@@ -304,7 +304,7 @@ import "github.com/klauspost/compress/zstd"
// Create a reader that caches decompressors. // Create a reader that caches decompressors.
// For this operation type we supply a nil Reader. // For this operation type we supply a nil Reader.
var decoder, _ = zstd.NewReader(nil, WithDecoderConcurrency(0)) var decoder, _ = zstd.NewReader(nil, zstd.WithDecoderConcurrency(0))
// Decompress a buffer. We don't supply a destination buffer, // Decompress a buffer. We don't supply a destination buffer,
// so it will be allocated by the decoder. // so it will be allocated by the decoder.

View File

@@ -592,7 +592,7 @@ func (b *blockDec) prepareSequences(in []byte, hist *history) (err error) {
} }
seq.fse.setRLE(symb) seq.fse.setRLE(symb)
if debugDecoder { if debugDecoder {
printf("RLE set to %+v, code: %v", symb, v) printf("RLE set to 0x%x, code: %v", symb, v)
} }
case compModeFSE: case compModeFSE:
println("Reading table for", tableIndex(i)) println("Reading table for", tableIndex(i))

View File

@@ -107,7 +107,7 @@ func WithDecoderDicts(dicts ...[]byte) DOption {
} }
} }
// WithEncoderDictRaw registers a dictionary that may be used by the decoder. // WithDecoderDictRaw registers a dictionary that may be used by the decoder.
// The slice content can be arbitrary data. // The slice content can be arbitrary data.
func WithDecoderDictRaw(id uint32, content []byte) DOption { func WithDecoderDictRaw(id uint32, content []byte) DOption {
return func(o *decoderOptions) error { return func(o *decoderOptions) error {

View File

@@ -133,8 +133,7 @@ encodeLoop:
if canRepeat && repIndex >= 0 && load3232(src, repIndex) == uint32(cv>>16) { if canRepeat && repIndex >= 0 && load3232(src, repIndex) == uint32(cv>>16) {
// Consider history as well. // Consider history as well.
var seq seq var seq seq
var length int32 length := 4 + e.matchlen(s+6, repIndex+4, src)
length = 4 + e.matchlen(s+6, repIndex+4, src)
seq.matchLen = uint32(length - zstdMinMatch) seq.matchLen = uint32(length - zstdMinMatch)
// We might be able to match backwards. // We might be able to match backwards.
@@ -645,8 +644,7 @@ encodeLoop:
if canRepeat && repIndex >= 0 && load3232(src, repIndex) == uint32(cv>>16) { if canRepeat && repIndex >= 0 && load3232(src, repIndex) == uint32(cv>>16) {
// Consider history as well. // Consider history as well.
var seq seq var seq seq
var length int32 length := 4 + e.matchlen(s+6, repIndex+4, src)
length = 4 + e.matchlen(s+6, repIndex+4, src)
seq.matchLen = uint32(length - zstdMinMatch) seq.matchLen = uint32(length - zstdMinMatch)

View File

@@ -129,7 +129,7 @@ func WithEncoderPadding(n int) EOption {
} }
// No need to waste our time. // No need to waste our time.
if n == 1 { if n == 1 {
o.pad = 0 n = 0
} }
if n > 1<<30 { if n > 1<<30 {
return fmt.Errorf("padding must less than 1GB (1<<30 bytes) ") return fmt.Errorf("padding must less than 1GB (1<<30 bytes) ")

View File

@@ -73,20 +73,20 @@ func (d *frameDec) reset(br byteBuffer) error {
switch err { switch err {
case io.EOF, io.ErrUnexpectedEOF: case io.EOF, io.ErrUnexpectedEOF:
return io.EOF return io.EOF
default:
return err
case nil: case nil:
signature[0] = b[0] signature[0] = b[0]
default:
return err
} }
// Read the rest, don't allow io.ErrUnexpectedEOF // Read the rest, don't allow io.ErrUnexpectedEOF
b, err = br.readSmall(3) b, err = br.readSmall(3)
switch err { switch err {
case io.EOF: case io.EOF:
return io.EOF return io.EOF
default:
return err
case nil: case nil:
copy(signature[1:], b) copy(signature[1:], b)
default:
return err
} }
if string(signature[1:4]) != skippableFrameMagic || signature[0]&0xf0 != 0x50 { if string(signature[1:4]) != skippableFrameMagic || signature[0]&0xf0 != 0x50 {

View File

@@ -0,0 +1,16 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
package zstd
// matchLen returns how many bytes match in a and b
//
// It assumes that:
//
// len(a) <= len(b) and len(a) > 0
//
//go:noescape
func matchLen(a []byte, b []byte) int

View File

@@ -0,0 +1,68 @@
// Copied from S2 implementation.
//go:build !appengine && !noasm && gc && !noasm
#include "textflag.h"
// func matchLen(a []byte, b []byte) int
// Requires: BMI
TEXT ·matchLen(SB), NOSPLIT, $0-56
MOVQ a_base+0(FP), AX
MOVQ b_base+24(FP), CX
MOVQ a_len+8(FP), DX
// matchLen
XORL SI, SI
CMPL DX, $0x08
JB matchlen_match4_standalone
matchlen_loopback_standalone:
MOVQ (AX)(SI*1), BX
XORQ (CX)(SI*1), BX
TESTQ BX, BX
JZ matchlen_loop_standalone
#ifdef GOAMD64_v3
TZCNTQ BX, BX
#else
BSFQ BX, BX
#endif
SARQ $0x03, BX
LEAL (SI)(BX*1), SI
JMP gen_match_len_end
matchlen_loop_standalone:
LEAL -8(DX), DX
LEAL 8(SI), SI
CMPL DX, $0x08
JAE matchlen_loopback_standalone
matchlen_match4_standalone:
CMPL DX, $0x04
JB matchlen_match2_standalone
MOVL (AX)(SI*1), BX
CMPL (CX)(SI*1), BX
JNE matchlen_match2_standalone
LEAL -4(DX), DX
LEAL 4(SI), SI
matchlen_match2_standalone:
CMPL DX, $0x02
JB matchlen_match1_standalone
MOVW (AX)(SI*1), BX
CMPW (CX)(SI*1), BX
JNE matchlen_match1_standalone
LEAL -2(DX), DX
LEAL 2(SI), SI
matchlen_match1_standalone:
CMPL DX, $0x01
JB gen_match_len_end
MOVB (AX)(SI*1), BL
CMPB (CX)(SI*1), BL
JNE gen_match_len_end
INCL SI
gen_match_len_end:
MOVQ SI, ret+48(FP)
RET

View File

@@ -0,0 +1,33 @@
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
package zstd
import (
"encoding/binary"
"math/bits"
)
// matchLen returns the maximum common prefix length of a and b.
// a must be the shortest of the two.
func matchLen(a, b []byte) (n int) {
for ; len(a) >= 8 && len(b) >= 8; a, b = a[8:], b[8:] {
diff := binary.LittleEndian.Uint64(a) ^ binary.LittleEndian.Uint64(b)
if diff != 0 {
return n + bits.TrailingZeros64(diff)>>3
}
n += 8
}
for i := range a {
if a[i] != b[i] {
break
}
n++
}
return n
}

View File

@@ -9,7 +9,6 @@ import (
"errors" "errors"
"log" "log"
"math" "math"
"math/bits"
) )
// enable debug printing // enable debug printing
@@ -106,27 +105,6 @@ func printf(format string, a ...interface{}) {
} }
} }
// matchLen returns the maximum common prefix length of a and b.
// a must be the shortest of the two.
func matchLen(a, b []byte) (n int) {
for ; len(a) >= 8 && len(b) >= 8; a, b = a[8:], b[8:] {
diff := binary.LittleEndian.Uint64(a) ^ binary.LittleEndian.Uint64(b)
if diff != 0 {
return n + bits.TrailingZeros64(diff)>>3
}
n += 8
}
for i := range a {
if a[i] != b[i] {
break
}
n++
}
return n
}
func load3232(b []byte, i int32) uint32 { func load3232(b []byte, i int32) uint32 {
return binary.LittleEndian.Uint32(b[:len(b):len(b)][i:]) return binary.LittleEndian.Uint32(b[:len(b):len(b)][i:])
} }

View File

@@ -61,7 +61,7 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_
SKIP_GOLANGCI_LINT := SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT := GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?= GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v1.49.0 GOLANGCI_LINT_VERSION ?= v1.51.2
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64. # golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
# windows isn't included here because of the path separator being different. # windows isn't included here because of the path separator being different.
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
@@ -91,6 +91,8 @@ BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))
SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG))
ifeq ($(GOHOSTARCH),amd64) ifeq ($(GOHOSTARCH),amd64)
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows))
# Only supported on amd64 # Only supported on amd64
@@ -205,7 +207,7 @@ common-tarball: promu
.PHONY: common-docker $(BUILD_DOCKER_ARCHS) .PHONY: common-docker $(BUILD_DOCKER_ARCHS)
common-docker: $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS)
$(BUILD_DOCKER_ARCHS): common-docker-%: $(BUILD_DOCKER_ARCHS): common-docker-%:
docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
-f $(DOCKERFILE_PATH) \ -f $(DOCKERFILE_PATH) \
--build-arg ARCH="$*" \ --build-arg ARCH="$*" \
--build-arg OS="linux" \ --build-arg OS="linux" \
@@ -214,19 +216,19 @@ $(BUILD_DOCKER_ARCHS): common-docker-%:
.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
common-docker-publish: $(PUBLISH_DOCKER_ARCHS) common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"
DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
common-docker-tag-latest: $(TAG_DOCKER_ARCHS) common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
.PHONY: common-docker-manifest .PHONY: common-docker-manifest
common-docker-manifest: common-docker-manifest:
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(DOCKER_IMAGE_TAG)) DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG))
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"
.PHONY: promu .PHONY: promu
promu: $(PROMU) promu: $(PROMU)

View File

@@ -21,6 +21,7 @@ import (
// kernel data structures. // kernel data structures.
type FS struct { type FS struct {
proc fs.FS proc fs.FS
real bool
} }
// DefaultMountPoint is the common mount point of the proc filesystem. // DefaultMountPoint is the common mount point of the proc filesystem.
@@ -39,5 +40,11 @@ func NewFS(mountPoint string) (FS, error) {
if err != nil { if err != nil {
return FS{}, err return FS{}, err
} }
return FS{fs}, nil
real, err := isRealProc(mountPoint)
if err != nil {
return FS{}, err
}
return FS{fs, real}, nil
} }

View File

@@ -0,0 +1,23 @@
// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build netbsd || openbsd || solaris || windows
// +build netbsd openbsd solaris windows
package procfs
// isRealProc returns true on architectures that don't have a Type argument
// in their Statfs_t struct
func isRealProc(mountPoint string) (bool, error) {
return true, nil
}

33
vendor/github.com/prometheus/procfs/fs_statfs_type.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !netbsd && !openbsd && !solaris && !windows
// +build !netbsd,!openbsd,!solaris,!windows
package procfs
import (
"syscall"
)
// isRealProc determines whether supplied mountpoint is really a proc filesystem.
func isRealProc(mountPoint string) (bool, error) {
stat := syscall.Statfs_t{}
err := syscall.Statfs(mountPoint, &stat)
if err != nil {
return false, err
}
// 0x9fa0 is PROC_SUPER_MAGIC: https://elixir.bootlin.com/linux/v6.1/source/include/uapi/linux/magic.h#L87
return stat.Type == 0x9fa0, nil
}

View File

@@ -64,6 +64,21 @@ func ParsePInt64s(ss []string) ([]*int64, error) {
return us, nil return us, nil
} }
// Parses a uint64 from given hex in string.
func ParseHexUint64s(ss []string) ([]*uint64, error) {
us := make([]*uint64, 0, len(ss))
for _, s := range ss {
u, err := strconv.ParseUint(s, 16, 64)
if err != nil {
return nil, err
}
us = append(us, &u)
}
return us, nil
}
// ReadUintFromFile reads a file and attempts to parse a uint64 from it. // ReadUintFromFile reads a file and attempts to parse a uint64 from it.
func ReadUintFromFile(path string) (uint64, error) { func ReadUintFromFile(path string) (uint64, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)

View File

@@ -186,6 +186,8 @@ type NFSOperationStats struct {
CumulativeTotalResponseMilliseconds uint64 CumulativeTotalResponseMilliseconds uint64
// Duration from when a request was enqueued to when it was completely handled. // Duration from when a request was enqueued to when it was completely handled.
CumulativeTotalRequestMilliseconds uint64 CumulativeTotalRequestMilliseconds uint64
// The average time from the point the client sends RPC requests until it receives the response.
AverageRTTMilliseconds float64
// The count of operations that complete with tk_status < 0. These statuses usually indicate error conditions. // The count of operations that complete with tk_status < 0. These statuses usually indicate error conditions.
Errors uint64 Errors uint64
} }
@@ -534,7 +536,6 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
ns = append(ns, n) ns = append(ns, n)
} }
opStats := NFSOperationStats{ opStats := NFSOperationStats{
Operation: strings.TrimSuffix(ss[0], ":"), Operation: strings.TrimSuffix(ss[0], ":"),
Requests: ns[0], Requests: ns[0],
@@ -546,6 +547,9 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
CumulativeTotalResponseMilliseconds: ns[6], CumulativeTotalResponseMilliseconds: ns[6],
CumulativeTotalRequestMilliseconds: ns[7], CumulativeTotalRequestMilliseconds: ns[7],
} }
if ns[0] != 0 {
opStats.AverageRTTMilliseconds = float64(ns[6]) / float64(ns[0])
}
if len(ns) > 8 { if len(ns) > 8 {
opStats.Errors = ns[8] opStats.Errors = ns[8]

View File

@@ -18,7 +18,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
"github.com/prometheus/procfs/internal/util" "github.com/prometheus/procfs/internal/util"
@@ -28,9 +27,13 @@ import (
// and contains netfilter conntrack statistics at one CPU core. // and contains netfilter conntrack statistics at one CPU core.
type ConntrackStatEntry struct { type ConntrackStatEntry struct {
Entries uint64 Entries uint64
Searched uint64
Found uint64 Found uint64
New uint64
Invalid uint64 Invalid uint64
Ignore uint64 Ignore uint64
Delete uint64
DeleteList uint64
Insert uint64 Insert uint64
InsertFailed uint64 InsertFailed uint64
Drop uint64 Drop uint64
@@ -81,73 +84,34 @@ func parseConntrackStat(r io.Reader) ([]ConntrackStatEntry, error) {
// Parses a ConntrackStatEntry from given array of fields. // Parses a ConntrackStatEntry from given array of fields.
func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) { func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) {
if len(fields) != 17 { entries, err := util.ParseHexUint64s(fields)
return nil, fmt.Errorf("invalid conntrackstat entry, missing fields")
}
entry := &ConntrackStatEntry{}
entries, err := parseConntrackStatField(fields[0])
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("invalid conntrackstat entry, couldn't parse fields: %s", err)
} }
entry.Entries = entries numEntries := len(entries)
if numEntries < 16 || numEntries > 17 {
found, err := parseConntrackStatField(fields[2]) return nil, fmt.Errorf("invalid conntrackstat entry, invalid number of fields: %d", numEntries)
if err != nil {
return nil, err
} }
entry.Found = found
invalid, err := parseConntrackStatField(fields[4]) stats := &ConntrackStatEntry{
if err != nil { Entries: *entries[0],
return nil, err Searched: *entries[1],
Found: *entries[2],
New: *entries[3],
Invalid: *entries[4],
Ignore: *entries[5],
Delete: *entries[6],
DeleteList: *entries[7],
Insert: *entries[8],
InsertFailed: *entries[9],
Drop: *entries[10],
EarlyDrop: *entries[11],
} }
entry.Invalid = invalid
ignore, err := parseConntrackStatField(fields[5]) // Ignore missing search_restart on Linux < 2.6.35.
if err != nil { if numEntries == 17 {
return nil, err stats.SearchRestart = *entries[16]
} }
entry.Ignore = ignore
insert, err := parseConntrackStatField(fields[8]) return stats, nil
if err != nil {
return nil, err
}
entry.Insert = insert
insertFailed, err := parseConntrackStatField(fields[9])
if err != nil {
return nil, err
}
entry.InsertFailed = insertFailed
drop, err := parseConntrackStatField(fields[10])
if err != nil {
return nil, err
}
entry.Drop = drop
earlyDrop, err := parseConntrackStatField(fields[11])
if err != nil {
return nil, err
}
entry.EarlyDrop = earlyDrop
searchRestart, err := parseConntrackStatField(fields[16])
if err != nil {
return nil, err
}
entry.SearchRestart = searchRestart
return entry, nil
}
// Parses a uint64 from given hex in string.
func parseConntrackStatField(field string) (uint64, error) {
val, err := strconv.ParseUint(field, 16, 64)
if err != nil {
return 0, fmt.Errorf("couldn't parse %q field: %w", field, err)
}
return val, err
} }

View File

@@ -76,6 +76,7 @@ func parseSoftnet(r io.Reader) ([]SoftnetStat, error) {
s := bufio.NewScanner(r) s := bufio.NewScanner(r)
var stats []SoftnetStat var stats []SoftnetStat
cpuIndex := 0
for s.Scan() { for s.Scan() {
columns := strings.Fields(s.Text()) columns := strings.Fields(s.Text())
width := len(columns) width := len(columns)
@@ -127,9 +128,13 @@ func parseSoftnet(r io.Reader) ([]SoftnetStat, error) {
softnetStat.SoftnetBacklogLen = us[0] softnetStat.SoftnetBacklogLen = us[0]
softnetStat.Index = us[1] softnetStat.Index = us[1]
} else {
// For older kernels, create the Index based on the scan line number.
softnetStat.Index = uint32(cpuIndex)
} }
softnetStat.Width = width softnetStat.Width = width
stats = append(stats, softnetStat) stats = append(stats, softnetStat)
cpuIndex++
} }
return stats, nil return stats, nil

182
vendor/github.com/prometheus/procfs/net_wireless.go generated vendored Normal file
View File

@@ -0,0 +1,182 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package procfs
import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/prometheus/procfs/internal/util"
)
// Wireless models the content of /proc/net/wireless.
type Wireless struct {
Name string
// Status is the current 4-digit hex value status of the interface.
Status uint64
// QualityLink is the link quality.
QualityLink int
// QualityLevel is the signal gain (dBm).
QualityLevel int
// QualityNoise is the signal noise baseline (dBm).
QualityNoise int
// DiscardedNwid is the number of discarded packets with wrong nwid/essid.
DiscardedNwid int
// DiscardedCrypt is the number of discarded packets with wrong code/decode (WEP).
DiscardedCrypt int
// DiscardedFrag is the number of discarded packets that can't perform MAC reassembly.
DiscardedFrag int
// DiscardedRetry is the number of discarded packets that reached max MAC retries.
DiscardedRetry int
// DiscardedMisc is the number of discarded packets for other reasons.
DiscardedMisc int
// MissedBeacon is the number of missed beacons/superframe.
MissedBeacon int
}
// Wireless returns kernel wireless statistics.
func (fs FS) Wireless() ([]*Wireless, error) {
b, err := util.ReadFileNoStat(fs.proc.Path("net/wireless"))
if err != nil {
return nil, err
}
m, err := parseWireless(bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("failed to parse wireless: %w", err)
}
return m, nil
}
// parseWireless parses the contents of /proc/net/wireless.
/*
Inter-| sta-| Quality | Discarded packets | Missed | WE
face | tus | link level noise | nwid crypt frag retry misc | beacon | 22
eth1: 0000 5. -256. -10. 0 1 0 3 0 0
eth2: 0000 5. -256. -20. 0 2 0 4 0 0
*/
func parseWireless(r io.Reader) ([]*Wireless, error) {
var (
interfaces []*Wireless
scanner = bufio.NewScanner(r)
)
for n := 0; scanner.Scan(); n++ {
// Skip the 2 header lines.
if n < 2 {
continue
}
line := scanner.Text()
parts := strings.Split(line, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("expected 2 parts after splitting line by ':', got %d for line %q", len(parts), line)
}
name := strings.TrimSpace(parts[0])
stats := strings.Fields(parts[1])
if len(stats) < 10 {
return nil, fmt.Errorf("invalid number of fields in line %d, expected at least 10, got %d: %q", n, len(stats), line)
}
status, err := strconv.ParseUint(stats[0], 16, 16)
if err != nil {
return nil, fmt.Errorf("invalid status in line %d: %q", n, line)
}
qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], "."))
if err != nil {
return nil, fmt.Errorf("failed to parse Quality:link as integer %q: %w", qlink, err)
}
qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], "."))
if err != nil {
return nil, fmt.Errorf("failed to parse Quality:level as integer %q: %w", qlevel, err)
}
qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], "."))
if err != nil {
return nil, fmt.Errorf("failed to parse Quality:noise as integer %q: %w", qnoise, err)
}
dnwid, err := strconv.Atoi(stats[4])
if err != nil {
return nil, fmt.Errorf("failed to parse Discarded:nwid as integer %q: %w", dnwid, err)
}
dcrypt, err := strconv.Atoi(stats[5])
if err != nil {
return nil, fmt.Errorf("failed to parse Discarded:crypt as integer %q: %w", dcrypt, err)
}
dfrag, err := strconv.Atoi(stats[6])
if err != nil {
return nil, fmt.Errorf("failed to parse Discarded:frag as integer %q: %w", dfrag, err)
}
dretry, err := strconv.Atoi(stats[7])
if err != nil {
return nil, fmt.Errorf("failed to parse Discarded:retry as integer %q: %w", dretry, err)
}
dmisc, err := strconv.Atoi(stats[8])
if err != nil {
return nil, fmt.Errorf("failed to parse Discarded:misc as integer %q: %w", dmisc, err)
}
mbeacon, err := strconv.Atoi(stats[9])
if err != nil {
return nil, fmt.Errorf("failed to parse Missed:beacon as integer %q: %w", mbeacon, err)
}
w := &Wireless{
Name: name,
Status: status,
QualityLink: qlink,
QualityLevel: qlevel,
QualityNoise: qnoise,
DiscardedNwid: dnwid,
DiscardedCrypt: dcrypt,
DiscardedFrag: dfrag,
DiscardedRetry: dretry,
DiscardedMisc: dmisc,
MissedBeacon: mbeacon,
}
interfaces = append(interfaces, w)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to scan /proc/net/wireless: %w", err)
}
return interfaces, nil
}

View File

@@ -15,7 +15,6 @@ package procfs
import ( import (
"bufio" "bufio"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -38,12 +37,7 @@ func (fs FS) NetStat() ([]NetStat, error) {
var netStatsTotal []NetStat var netStatsTotal []NetStat
for _, filePath := range statFiles { for _, filePath := range statFiles {
file, err := os.Open(filePath) procNetstat, err := parseNetstat(filePath)
if err != nil {
return nil, err
}
procNetstat, err := parseNetstat(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -56,14 +50,17 @@ func (fs FS) NetStat() ([]NetStat, error) {
// parseNetstat parses the metrics from `/proc/net/stat/` file // parseNetstat parses the metrics from `/proc/net/stat/` file
// and returns a NetStat structure. // and returns a NetStat structure.
func parseNetstat(r io.Reader) (NetStat, error) { func parseNetstat(filePath string) (NetStat, error) {
var ( netStat := NetStat{
scanner = bufio.NewScanner(r)
netStat = NetStat{
Stats: make(map[string][]uint64), Stats: make(map[string][]uint64),
} }
) file, err := os.Open(filePath)
if err != nil {
return netStat, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Scan() scanner.Scan()
// First string is always a header for stats // First string is always a header for stats

View File

@@ -21,7 +21,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/prometheus/procfs/internal/fs"
"github.com/prometheus/procfs/internal/util" "github.com/prometheus/procfs/internal/util"
) )
@@ -30,7 +29,7 @@ type Proc struct {
// The process ID. // The process ID.
PID int PID int
fs fs.FS fs FS
} }
// Procs represents a list of Proc structs. // Procs represents a list of Proc structs.
@@ -92,7 +91,7 @@ func (fs FS) Proc(pid int) (Proc, error) {
if _, err := os.Stat(fs.proc.Path(strconv.Itoa(pid))); err != nil { if _, err := os.Stat(fs.proc.Path(strconv.Itoa(pid))); err != nil {
return Proc{}, err return Proc{}, err
} }
return Proc{PID: pid, fs: fs.proc}, nil return Proc{PID: pid, fs: fs}, nil
} }
// AllProcs returns a list of all currently available processes. // AllProcs returns a list of all currently available processes.
@@ -114,7 +113,7 @@ func (fs FS) AllProcs() (Procs, error) {
if err != nil { if err != nil {
continue continue
} }
p = append(p, Proc{PID: int(pid), fs: fs.proc}) p = append(p, Proc{PID: int(pid), fs: fs})
} }
return p, nil return p, nil
@@ -237,6 +236,19 @@ func (p Proc) FileDescriptorTargets() ([]string, error) {
// FileDescriptorsLen returns the number of currently open file descriptors of // FileDescriptorsLen returns the number of currently open file descriptors of
// a process. // a process.
func (p Proc) FileDescriptorsLen() (int, error) { func (p Proc) FileDescriptorsLen() (int, error) {
// Use fast path if available (Linux v6.2): https://github.com/torvalds/linux/commit/f1f1f2569901
if p.fs.real {
stat, err := os.Stat(p.path("fd"))
if err != nil {
return 0, err
}
size := stat.Size()
if size > 0 {
return int(size), nil
}
}
fds, err := p.fileDescriptors() fds, err := p.fileDescriptors()
if err != nil { if err != nil {
return 0, err return 0, err
@@ -285,7 +297,7 @@ func (p Proc) fileDescriptors() ([]string, error) {
} }
func (p Proc) path(pa ...string) string { func (p Proc) path(pa ...string) string {
return p.fs.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...) return p.fs.proc.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
} }
// FileDescriptorsInfo retrieves information about all file descriptors of // FileDescriptorsInfo retrieves information about all file descriptors of

View File

@@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/prometheus/procfs/internal/fs"
"github.com/prometheus/procfs/internal/util" "github.com/prometheus/procfs/internal/util"
) )
@@ -112,7 +111,7 @@ type ProcStat struct {
// Aggregated block I/O delays, measured in clock ticks (centiseconds). // Aggregated block I/O delays, measured in clock ticks (centiseconds).
DelayAcctBlkIOTicks uint64 DelayAcctBlkIOTicks uint64
proc fs.FS proc FS
} }
// NewStat returns the current status information of the process. // NewStat returns the current status information of the process.
@@ -210,8 +209,7 @@ func (s ProcStat) ResidentMemory() int {
// StartTime returns the unix timestamp of the process in seconds. // StartTime returns the unix timestamp of the process in seconds.
func (s ProcStat) StartTime() (float64, error) { func (s ProcStat) StartTime() (float64, error) {
fs := FS{proc: s.proc} stat, err := s.proc.Stat()
stat, err := fs.Stat()
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@@ -15,6 +15,7 @@ package procfs
import ( import (
"bytes" "bytes"
"sort"
"strconv" "strconv"
"strings" "strings"
@@ -76,6 +77,9 @@ type ProcStatus struct {
UIDs [4]string UIDs [4]string
// GIDs of the process (Real, effective, saved set, and filesystem GIDs) // GIDs of the process (Real, effective, saved set, and filesystem GIDs)
GIDs [4]string GIDs [4]string
// CpusAllowedList: List of cpu cores processes are allowed to run on.
CpusAllowedList []uint64
} }
// NewStatus returns the current status information of the process. // NewStatus returns the current status information of the process.
@@ -161,10 +165,38 @@ func (s *ProcStatus) fillStatus(k string, vString string, vUint uint64, vUintByt
s.VoluntaryCtxtSwitches = vUint s.VoluntaryCtxtSwitches = vUint
case "nonvoluntary_ctxt_switches": case "nonvoluntary_ctxt_switches":
s.NonVoluntaryCtxtSwitches = vUint s.NonVoluntaryCtxtSwitches = vUint
case "Cpus_allowed_list":
s.CpusAllowedList = calcCpusAllowedList(vString)
} }
} }
// TotalCtxtSwitches returns the total context switch. // TotalCtxtSwitches returns the total context switch.
func (s ProcStatus) TotalCtxtSwitches() uint64 { func (s ProcStatus) TotalCtxtSwitches() uint64 {
return s.VoluntaryCtxtSwitches + s.NonVoluntaryCtxtSwitches return s.VoluntaryCtxtSwitches + s.NonVoluntaryCtxtSwitches
} }
func calcCpusAllowedList(cpuString string) []uint64 {
s := strings.Split(cpuString, ",")
var g []uint64
for _, cpu := range s {
// parse cpu ranges, example: 1-3=[1,2,3]
if l := strings.Split(strings.TrimSpace(cpu), "-"); len(l) > 1 {
startCPU, _ := strconv.ParseUint(l[0], 10, 64)
endCPU, _ := strconv.ParseUint(l[1], 10, 64)
for i := startCPU; i <= endCPU; i++ {
g = append(g, i)
}
} else if len(l) == 1 {
cpu, _ := strconv.ParseUint(l[0], 10, 64)
g = append(g, cpu)
}
}
sort.Slice(g, func(i, j int) bool { return g[i] < g[j] })
return g
}

View File

@@ -54,7 +54,8 @@ func (fs FS) AllThreads(pid int) (Procs, error) {
if err != nil { if err != nil {
continue continue
} }
t = append(t, Proc{PID: int(tid), fs: fsi.FS(taskPath)})
t = append(t, Proc{PID: int(tid), fs: FS{fsi.FS(taskPath), fs.real}})
} }
return t, nil return t, nil
@@ -66,13 +67,13 @@ func (fs FS) Thread(pid, tid int) (Proc, error) {
if _, err := os.Stat(taskPath); err != nil { if _, err := os.Stat(taskPath); err != nil {
return Proc{}, err return Proc{}, err
} }
return Proc{PID: tid, fs: fsi.FS(taskPath)}, nil return Proc{PID: tid, fs: FS{fsi.FS(taskPath), fs.real}}, nil
} }
// Thread returns a process for a given TID of Proc. // Thread returns a process for a given TID of Proc.
func (proc Proc) Thread(tid int) (Proc, error) { func (proc Proc) Thread(tid int) (Proc, error) {
tfs := fsi.FS(proc.path("task")) tfs := FS{fsi.FS(proc.path("task")), proc.fs.real}
if _, err := os.Stat(tfs.Path(strconv.Itoa(tid))); err != nil { if _, err := os.Stat(tfs.proc.Path(strconv.Itoa(tid))); err != nil {
return Proc{}, err return Proc{}, err
} }
return Proc{PID: tid, fs: tfs}, nil return Proc{PID: tid, fs: tfs}, nil

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"io" "io"
"runtime" "runtime"
"strings"
) )
// Writer at INFO level. See WriterLevel for details. // Writer at INFO level. See WriterLevel for details.
@@ -20,15 +21,18 @@ func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
return NewEntry(logger).WriterLevel(level) return NewEntry(logger).WriterLevel(level)
} }
// Writer returns an io.Writer that writes to the logger at the info log level
func (entry *Entry) Writer() *io.PipeWriter { func (entry *Entry) Writer() *io.PipeWriter {
return entry.WriterLevel(InfoLevel) return entry.WriterLevel(InfoLevel)
} }
// WriterLevel returns an io.Writer that writes to the logger at the given log level
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe() reader, writer := io.Pipe()
var printFunc func(args ...interface{}) var printFunc func(args ...interface{})
// Determine which log function to use based on the specified log level
switch level { switch level {
case TraceLevel: case TraceLevel:
printFunc = entry.Trace printFunc = entry.Trace
@@ -48,23 +52,51 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
printFunc = entry.Print printFunc = entry.Print
} }
// Start a new goroutine to scan the input and write it to the logger using the specified print function.
// It splits the input into chunks of up to 64KB to avoid buffer overflows.
go entry.writerScanner(reader, printFunc) go entry.writerScanner(reader, printFunc)
// Set a finalizer function to close the writer when it is garbage collected
runtime.SetFinalizer(writer, writerFinalizer) runtime.SetFinalizer(writer, writerFinalizer)
return writer return writer
} }
// writerScanner scans the input from the reader and writes it to the logger
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) { func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
for scanner.Scan() {
printFunc(scanner.Text()) // Set the buffer size to the maximum token size to avoid buffer overflows
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), bufio.MaxScanTokenSize)
// Define a split function to split the input into chunks of up to 64KB
chunkSize := bufio.MaxScanTokenSize // 64KB
splitFunc := func(data []byte, atEOF bool) (int, []byte, error) {
if len(data) >= chunkSize {
return chunkSize, data[:chunkSize], nil
} }
return bufio.ScanLines(data, atEOF)
}
// Use the custom split function to split the input
scanner.Split(splitFunc)
// Scan the input and write it to the logger using the specified print function
for scanner.Scan() {
printFunc(strings.TrimRight(scanner.Text(), "\r\n"))
}
// If there was an error while scanning the input, log an error
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
entry.Errorf("Error while reading from Writer: %s", err) entry.Errorf("Error while reading from Writer: %s", err)
} }
// Close the reader when we are done
reader.Close() reader.Close()
} }
// WriterFinalizer is a finalizer function that closes then given writer when it is garbage collected
func writerFinalizer(writer *io.PipeWriter) { func writerFinalizer(writer *io.PipeWriter) {
writer.Close() writer.Close()
} }

Some files were not shown because too many files have changed in this diff Show More