diff --git a/cmd/decode.go b/cmd/decode.go index 4a54668..7cb677f 100644 --- a/cmd/decode.go +++ b/cmd/decode.go @@ -4,7 +4,7 @@ import ( "encoding/hex" "fmt" - "github.com/lbryio/lbry.go/v2/schema/claim" + "github.com/lbryio/lbry.go/v2/schema/stake" "github.com/davecgh/go-spew/spew" "github.com/golang/protobuf/jsonpb" @@ -23,7 +23,7 @@ func init() { } func decodeCmd(cmd *cobra.Command, args []string) { - c, err := claim.DecodeClaimHex(args[0], "") + c, err := stake.DecodeClaimHex(args[0], "") if err != nil { log.Fatal(err) } diff --git a/cmd/getstream.go b/cmd/getstream.go index c02ad0b..1c25df5 100644 --- a/cmd/getstream.go +++ b/cmd/getstream.go @@ -41,7 +41,7 @@ func getStreamCmd(cmd *cobra.Command, args []string) { var sd stream.SDBlob - sdb, err := s.Get(sdHash) + sdb, _, err := s.Get(sdHash) if err != nil { log.Fatal(err) } @@ -62,7 +62,7 @@ func getStreamCmd(cmd *cobra.Command, args []string) { } for i := 0; i < len(sd.BlobInfos)-1; i++ { - b, err := s.Get(hex.EncodeToString(sd.BlobInfos[i].BlobHash)) + b, _, err := s.Get(hex.EncodeToString(sd.BlobInfos[i].BlobHash)) if err != nil { log.Fatal(err) } diff --git a/cmd/integrity.go b/cmd/integrity.go new file mode 100644 index 0000000..8be9e2b --- /dev/null +++ b/cmd/integrity.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "crypto/sha512" + "encoding/hex" + "io/ioutil" + "os" + "path" + "runtime" + "sync/atomic" + "time" + + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/reflector.go/meta" + "github.com/lbryio/reflector.go/store/speedwalk" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var threads int + +func init() { + var cmd = &cobra.Command{ + Use: "check-integrity", + Short: "check blobs integrity for a given path", + Run: integrityCheckCmd, + } + cmd.Flags().StringVar(&diskStorePath, "store-path", "", "path of the store where all blobs are cached") + cmd.Flags().IntVar(&threads, "threads", runtime.NumCPU()-1, "number of concurrent threads to process blobs") + rootCmd.AddCommand(cmd) +} + +func integrityCheckCmd(cmd *cobra.Command, args []string) { + log.Printf("reflector %s", meta.VersionString()) + if diskStorePath == "" { + log.Fatal("store-path must be defined") + } + + blobs, err := speedwalk.AllFiles(diskStorePath, true) + if err != nil { + log.Errorf("error while reading blobs from disk %s", errors.FullTrace(err)) + } + tasks := make(chan string, len(blobs)) + done := make(chan bool) + processed := new(int32) + go produce(tasks, blobs) + cpus := runtime.NumCPU() + for i := 0; i < cpus-1; i++ { + go consume(i, tasks, done, len(blobs), processed) + } + <-done +} + +func produce(tasks chan<- string, blobs []string) { + for _, b := range blobs { + tasks <- b + } + close(tasks) +} + +func consume(worker int, tasks <-chan string, done chan<- bool, totalTasks int, processed *int32) { + start := time.Now() + + for b := range tasks { + checked := atomic.AddInt32(processed, 1) + if worker == 0 { + remaining := int32(totalTasks) - checked + timePerBlob := time.Since(start).Microseconds() / int64(checked) + remainingTime := time.Duration(int64(remaining)*timePerBlob) * time.Microsecond + log.Infof("[T%d] %d/%d blobs checked. ETA: %s", worker, checked, totalTasks, remainingTime.String()) + } + blobPath := path.Join(diskStorePath, b[:2], b) + blob, err := ioutil.ReadFile(blobPath) + if err != nil { + if os.IsNotExist(err) { + continue + } + log.Errorf("[Worker %d] Error looking up blob %s: %s", worker, b, err.Error()) + continue + } + hashBytes := sha512.Sum384(blob) + readHash := hex.EncodeToString(hashBytes[:]) + if readHash != b { + log.Infof("[%s] found a broken blob while reading from disk. Actual hash: %s", b, readHash) + err := os.Remove(blobPath) + if err != nil { + log.Errorf("Error while deleting broken blob %s: %s", b, err.Error()) + } + } + } + done <- true +} diff --git a/cmd/peer.go b/cmd/peer.go index 3cb2a19..c631bfe 100644 --- a/cmd/peer.go +++ b/cmd/peer.go @@ -33,11 +33,13 @@ func peerCmd(cmd *cobra.Command, args []string) { peerServer := peer.NewServer(s3) if !peerNoDB { - db := new(db.SQL) + db := &db.SQL{ + LogQueries: log.GetLevel() == log.DebugLevel, + } err = db.Connect(globalConfig.DBConn) checkErr(err) - combo := store.NewDBBackedStore(s3, db) + combo := store.NewDBBackedStore(s3, db, false) peerServer = peer.NewServer(combo) } diff --git a/cmd/populatedb.go b/cmd/populatedb.go new file mode 100644 index 0000000..7199e42 --- /dev/null +++ b/cmd/populatedb.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/reflector.go/db" + "github.com/lbryio/reflector.go/meta" + "github.com/lbryio/reflector.go/store/speedwalk" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + diskStorePath string +) + +func init() { + var cmd = &cobra.Command{ + Use: "populate-db", + Short: "populate local database with blobs from a disk storage", + Run: populateDbCmd, + } + cmd.Flags().StringVar(&diskStorePath, "store-path", "", + "path of the store where all blobs are cached") + rootCmd.AddCommand(cmd) +} + +func populateDbCmd(cmd *cobra.Command, args []string) { + log.Printf("reflector %s", meta.VersionString()) + if diskStorePath == "" { + log.Fatal("store-path must be defined") + } + localDb := &db.SQL{ + SoftDelete: true, + TrackAccess: db.TrackAccessBlobs, + LogQueries: log.GetLevel() == log.DebugLevel, + } + err := localDb.Connect("reflector:reflector@tcp(localhost:3306)/reflector") + if err != nil { + log.Fatal(err) + } + blobs, err := speedwalk.AllFiles(diskStorePath, true) + err = localDb.AddBlobs(blobs) + if err != nil { + log.Errorf("error while storing to db: %s", errors.FullTrace(err)) + } +} diff --git a/cmd/reflector.go b/cmd/reflector.go index 2f687d0..087c918 100644 --- a/cmd/reflector.go +++ b/cmd/reflector.go @@ -8,33 +8,42 @@ import ( "syscall" "time" + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/lbry.go/v2/extras/stop" "github.com/lbryio/reflector.go/db" "github.com/lbryio/reflector.go/internal/metrics" "github.com/lbryio/reflector.go/meta" "github.com/lbryio/reflector.go/peer" "github.com/lbryio/reflector.go/peer/http3" "github.com/lbryio/reflector.go/reflector" + "github.com/lbryio/reflector.go/server/http" "github.com/lbryio/reflector.go/store" + "github.com/lbryio/lbry.go/v2/stream" + + "github.com/c2h5oh/datasize" log "github.com/sirupsen/logrus" - "github.com/spf13/cast" "github.com/spf13/cobra" ) var ( - tcpPeerPort int - http3PeerPort int - receiverPort int - metricsPort int - disableUploads bool - disableBlocklist bool - proxyAddress string - proxyPort string - proxyProtocol string - useDB bool - cloudFrontEndpoint string - reflectorCmdDiskCache string - reflectorCmdMemCache int + tcpPeerPort int + http3PeerPort int + httpPort int + receiverPort int + metricsPort int + disableUploads bool + disableBlocklist bool + proxyAddress string + proxyPort string + proxyProtocol string + useDB bool + cloudFrontEndpoint string + WasabiEndpoint string + reflectorCmdDiskCache string + bufferReflectorCmdDiskCache string + reflectorCmdMemCache int + requestQueueSize int ) func init() { @@ -47,28 +56,34 @@ func init() { cmd.Flags().StringVar(&proxyPort, "proxy-port", "5567", "port of another reflector server where blobs are fetched from") cmd.Flags().StringVar(&proxyProtocol, "proxy-protocol", "http3", "protocol used to fetch blobs from another reflector server (tcp/http3)") cmd.Flags().StringVar(&cloudFrontEndpoint, "cloudfront-endpoint", "", "CloudFront edge endpoint for standard HTTP retrieval") + cmd.Flags().StringVar(&WasabiEndpoint, "wasabi-endpoint", "", "Wasabi edge endpoint for standard HTTP retrieval") cmd.Flags().IntVar(&tcpPeerPort, "tcp-peer-port", 5567, "The port reflector will distribute content from") cmd.Flags().IntVar(&http3PeerPort, "http3-peer-port", 5568, "The port reflector will distribute content from over HTTP3 protocol") + cmd.Flags().IntVar(&httpPort, "http-port", 5569, "The port reflector will distribute content from over HTTP protocol") cmd.Flags().IntVar(&receiverPort, "receiver-port", 5566, "The port reflector will receive content from") cmd.Flags().IntVar(&metricsPort, "metrics-port", 2112, "The port reflector will use for metrics") + cmd.Flags().IntVar(&requestQueueSize, "request-queue-size", 200, "How many concurrent requests should be submitted to upstream") cmd.Flags().BoolVar(&disableUploads, "disable-uploads", false, "Disable uploads to this reflector server") cmd.Flags().BoolVar(&disableBlocklist, "disable-blocklist", false, "Disable blocklist watching/updating") cmd.Flags().BoolVar(&useDB, "use-db", true, "whether to connect to the reflector db or not") cmd.Flags().StringVar(&reflectorCmdDiskCache, "disk-cache", "", - "enable disk cache, setting max size and path where to store blobs. format is 'MAX_BLOBS:CACHE_PATH'") + "enable disk cache, setting max size and path where to store blobs. format is 'sizeGB:CACHE_PATH'") + cmd.Flags().StringVar(&bufferReflectorCmdDiskCache, "buffer-disk-cache", "", + "enable buffer disk cache, setting max size and path where to store blobs. format is 'sizeGB:CACHE_PATH'") cmd.Flags().IntVar(&reflectorCmdMemCache, "mem-cache", 0, "enable in-memory cache with a max size of this many blobs") rootCmd.AddCommand(cmd) } func reflectorCmd(cmd *cobra.Command, args []string) { log.Printf("reflector %s", meta.VersionString()) + cleanerStopper := stop.New() // the blocklist logic requires the db backed store to be the outer-most store underlyingStore := setupStore() - outerStore := wrapWithCache(underlyingStore) + outerStore := wrapWithCache(underlyingStore, cleanerStopper) if !disableUploads { - reflectorServer := reflector.NewServer(underlyingStore) + reflectorServer := reflector.NewServer(underlyingStore, outerStore) reflectorServer.Timeout = 3 * time.Minute reflectorServer.EnableBlocklist = !disableBlocklist @@ -86,21 +101,31 @@ func reflectorCmd(cmd *cobra.Command, args []string) { } defer peerServer.Shutdown() - http3PeerServer := http3.NewServer(outerStore) + http3PeerServer := http3.NewServer(outerStore, requestQueueSize) err = http3PeerServer.Start(":" + strconv.Itoa(http3PeerPort)) if err != nil { log.Fatal(err) } defer http3PeerServer.Shutdown() + httpServer := http.NewServer(outerStore) + err = httpServer.Start(":" + strconv.Itoa(httpPort)) + if err != nil { + log.Fatal(err) + } + defer httpServer.Shutdown() + metricsServer := metrics.NewServer(":"+strconv.Itoa(metricsPort), "/metrics") metricsServer.Start() defer metricsServer.Shutdown() + defer outerStore.Shutdown() + defer underlyingStore.Shutdown() interruptChan := make(chan os.Signal, 1) signal.Notify(interruptChan, os.Interrupt, syscall.SIGTERM) <-interruptChan // deferred shutdowns happen now + cleanerStopper.StopAndWait() } func setupStore() store.BlobStore { @@ -118,45 +143,89 @@ func setupStore() store.BlobStore { Address: proxyAddress + ":" + proxyPort, Timeout: 30 * time.Second, }) + case "http": + s = store.NewHttpStore(proxyAddress + ":" + proxyPort) default: log.Fatalf("protocol is not recognized: %s", proxyProtocol) } } else { - s3Store := store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName) - if cloudFrontEndpoint != "" { - s = store.NewCloudFrontRWStore(store.NewCloudFrontROStore(cloudFrontEndpoint), s3Store) - } else { + var s3Store *store.S3Store + if conf != "none" { + s3Store = store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName) + } + if cloudFrontEndpoint != "" && WasabiEndpoint != "" { + ittt := store.NewITTTStore(store.NewCloudFrontROStore(WasabiEndpoint), store.NewCloudFrontROStore(cloudFrontEndpoint)) + if s3Store != nil { + s = store.NewCloudFrontRWStore(ittt, s3Store) + } else { + s = ittt + } + } else if s3Store != nil { s = s3Store + } else { + log.Fatalf("this configuration does not include a valid upstream source") } } if useDB { - db := new(db.SQL) - db.TrackAccessTime = true - err := db.Connect(globalConfig.DBConn) + dbInst := &db.SQL{ + TrackAccess: db.TrackAccessStreams, + LogQueries: log.GetLevel() == log.DebugLevel, + } + err := dbInst.Connect(globalConfig.DBConn) if err != nil { log.Fatal(err) } - s = store.NewDBBackedStore(s, db) + s = store.NewDBBackedStore(s, dbInst, false) } return s } -func wrapWithCache(s store.BlobStore) store.BlobStore { +func wrapWithCache(s store.BlobStore, cleanerStopper *stop.Group) store.BlobStore { wrapped := s - diskCacheMaxSize, diskCachePath := diskCacheParams() + diskCacheMaxSize, diskCachePath := diskCacheParams(reflectorCmdDiskCache) + //we are tracking blobs in memory with a 1 byte long boolean, which means that for each 2MB (a blob) we need 1Byte + // so if the underlying cache holds 10MB, 10MB/2MB=5Bytes which is also the exact count of objects to restore on startup + realCacheSize := float64(diskCacheMaxSize) / float64(stream.MaxBlobSize) if diskCacheMaxSize > 0 { err := os.MkdirAll(diskCachePath, os.ModePerm) if err != nil { log.Fatal(err) } + + localDb := &db.SQL{ + SoftDelete: true, + TrackAccess: db.TrackAccessBlobs, + LogQueries: log.GetLevel() == log.DebugLevel, + } + err = localDb.Connect("reflector:reflector@tcp(localhost:3306)/reflector") + if err != nil { + log.Fatal(err) + } + dbBackedDiskStore := store.NewDBBackedStore(store.NewDiskStore(diskCachePath, 2), localDb, true) wrapped = store.NewCachingStore( "reflector", wrapped, - store.NewLRUStore("peer_server", store.NewDiskStore(diskCachePath, 2), diskCacheMaxSize), + dbBackedDiskStore, + ) + + go cleanOldestBlobs(int(realCacheSize), localDb, dbBackedDiskStore, cleanerStopper) + } + + diskCacheMaxSize, diskCachePath = diskCacheParams(bufferReflectorCmdDiskCache) + realCacheSize = float64(diskCacheMaxSize) / float64(stream.MaxBlobSize) + if diskCacheMaxSize > 0 { + err := os.MkdirAll(diskCachePath, os.ModePerm) + if err != nil { + log.Fatal(err) + } + wrapped = store.NewCachingStore( + "reflector", + wrapped, + store.NewLFUDAStore("nvme", store.NewDiskStore(diskCachePath, 2), realCacheSize), ) } @@ -164,32 +233,105 @@ func wrapWithCache(s store.BlobStore) store.BlobStore { wrapped = store.NewCachingStore( "reflector", wrapped, - store.NewLRUStore("peer_server", store.NewMemStore(), reflectorCmdMemCache), + store.NewLRUStore("mem", store.NewMemStore(), reflectorCmdMemCache), ) } return wrapped } -func diskCacheParams() (int, string) { - if reflectorCmdDiskCache == "" { +func diskCacheParams(diskParams string) (int, string) { + if diskParams == "" { return 0, "" } - parts := strings.Split(reflectorCmdDiskCache, ":") + parts := strings.Split(diskParams, ":") if len(parts) != 2 { log.Fatalf("--disk-cache must be a number, followed by ':', followed by a string") } - maxSize := cast.ToInt(parts[0]) - if maxSize <= 0 { - log.Fatalf("--disk-cache max size must be more than 0") - } - + diskCacheSize := parts[0] path := parts[1] if len(path) == 0 || path[0] != '/' { log.Fatalf("--disk-cache path must start with '/'") } - return maxSize, path + var maxSize datasize.ByteSize + err := maxSize.UnmarshalText([]byte(diskCacheSize)) + if err != nil { + log.Fatal(err) + } + if maxSize <= 0 { + log.Fatal("--disk-cache size must be more than 0") + } + return int(maxSize), path +} + +func cleanOldestBlobs(maxItems int, db *db.SQL, store store.BlobStore, stopper *stop.Group) { + // this is so that it runs on startup without having to wait for 10 minutes + err := doClean(maxItems, db, store, stopper) + if err != nil { + log.Error(errors.FullTrace(err)) + } + const cleanupInterval = 10 * time.Minute + for { + select { + case <-stopper.Ch(): + log.Infoln("stopping self cleanup") + return + case <-time.After(cleanupInterval): + err := doClean(maxItems, db, store, stopper) + if err != nil { + log.Error(errors.FullTrace(err)) + } + } + } +} + +func doClean(maxItems int, db *db.SQL, store store.BlobStore, stopper *stop.Group) error { + blobsCount, err := db.Count() + if err != nil { + return err + } + + if blobsCount >= maxItems { + itemsToDelete := blobsCount / 10 + blobs, err := db.LeastRecentlyAccessedHashes(itemsToDelete) + if err != nil { + return err + } + blobsChan := make(chan string, len(blobs)) + wg := &stop.Group{} + go func() { + for _, hash := range blobs { + select { + case <-stopper.Ch(): + return + default: + } + blobsChan <- hash + } + close(blobsChan) + }() + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for h := range blobsChan { + select { + case <-stopper.Ch(): + return + default: + } + err = store.Delete(h) + if err != nil { + log.Errorf("error pruning %s: %s", h, errors.FullTrace(err)) + continue + } + } + }() + } + wg.Wait() + } + return nil } diff --git a/cmd/root.go b/cmd/root.go index ce28f8f..1e422a7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -163,3 +163,9 @@ func mustGetFlagInt64(cmd *cobra.Command, name string) int64 { checkErr(err) return v } + +func mustGetFlagBool(cmd *cobra.Command, name string) bool { + v, err := cmd.Flags().GetBool(name) + checkErr(err) + return v +} diff --git a/cmd/send.go b/cmd/send.go new file mode 100644 index 0000000..7021866 --- /dev/null +++ b/cmd/send.go @@ -0,0 +1,160 @@ +package cmd + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/signal" + "path" + "syscall" + + "github.com/lbryio/reflector.go/reflector" + + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/lbry.go/v2/stream" + + "github.com/spf13/cobra" +) + +func init() { + var cmd = &cobra.Command{ + Use: "send ADDRESS:PORT PATH", + Short: "Send a file to a reflector", + Args: cobra.ExactArgs(2), + Run: sendCmd, + } + cmd.PersistentFlags().String("sd-cache", "", "path to dir where sd blobs will be cached") + rootCmd.AddCommand(cmd) +} + +// todo: if retrying a large file is slow, we can add the ability to seek ahead in the file so we're not +// re-uploading blobs that already exist + +var hackyReflector reflector.Client + +func sendCmd(cmd *cobra.Command, args []string) { + reflectorAddress := args[0] + err := hackyReflector.Connect(reflectorAddress) + checkErr(err) + defer hackyReflector.Close() + + filePath := args[1] + file, err := os.Open(filePath) + checkErr(err) + defer file.Close() + + sdCachePath := "" + sdCacheDir := mustGetFlagString(cmd, "sd-cache") + if sdCacheDir != "" { + if _, err := os.Stat(sdCacheDir); os.IsNotExist(err) { + err = os.MkdirAll(sdCacheDir, 0777) + checkErr(err) + } + sdCachePath = path.Join(sdCacheDir, filePath+".sdblob") + } + + var enc *stream.Encoder + + if sdCachePath != "" { + if _, err := os.Stat(sdCachePath); !os.IsNotExist(err) { + sdBlob, err := ioutil.ReadFile(sdCachePath) + checkErr(err) + cachedSDBlob := &stream.SDBlob{} + err = cachedSDBlob.FromBlob(sdBlob) + checkErr(err) + enc = stream.NewEncoderFromSD(file, cachedSDBlob) + } + } + if enc == nil { + enc = stream.NewEncoder(file) + } + + exitCode := 0 + + var killed bool + interruptChan := make(chan os.Signal, 1) + signal.Notify(interruptChan, os.Interrupt, syscall.SIGTERM) + go func() { + sig := <-interruptChan + fmt.Printf("caught %s, exiting...\n", sig.String()) + killed = true + exitCode = 1 + }() + + for { + if killed { + break + } + + b, err := enc.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + fmt.Printf("error reading next blob: %v\n", err) + exitCode = 1 + break + } + + err = hackyReflect(b, false) + if err != nil { + fmt.Printf("error reflecting blob %s: %v\n", b.HashHex()[:8], err) + exitCode = 1 + break + } + } + + sd := enc.SDBlob() + //sd.StreamName = filepath.Base(filePath) + //sd.SuggestedFileName = filepath.Base(filePath) + err = ioutil.WriteFile(sdCachePath, sd.ToBlob(), 0666) + if err != nil { + fmt.Printf("error saving sd blob: %v\n", err) + fmt.Println(sd.ToJson()) + exitCode = 1 + } + + if killed { + os.Exit(exitCode) + } + + if reflectorAddress != "" { + err = hackyReflect(sd.ToBlob(), true) + if err != nil { + fmt.Printf("error reflecting sd blob %s: %v\n", sd.HashHex()[:8], err) + exitCode = 1 + } + } + + ret := struct { + SDHash string `json:"sd_hash"` + SourceHash string `json:"source_hash"` + }{ + SDHash: sd.HashHex(), + SourceHash: hex.EncodeToString(enc.SourceHash()), + } + + j, err := json.MarshalIndent(ret, "", " ") + checkErr(err) + fmt.Println(string(j)) + os.Exit(exitCode) +} + +func hackyReflect(b stream.Blob, sd bool) error { + var err error + if sd { + err = hackyReflector.SendSDBlob(b) + } else { + err = hackyReflector.SendBlob(b) + } + + if errors.Is(err, reflector.ErrBlobExists) { + //fmt.Printf("%s already reflected\n", b.HashHex()[:8]) + return nil + } + + return err +} diff --git a/cmd/sendblob.go b/cmd/sendblob.go index 3cd9f05..f37f390 100644 --- a/cmd/sendblob.go +++ b/cmd/sendblob.go @@ -2,7 +2,6 @@ package cmd import ( "crypto/rand" - "io/ioutil" "os" "github.com/lbryio/reflector.go/reflector" @@ -52,9 +51,8 @@ func sendBlobCmd(cmd *cobra.Command, args []string) { file, err := os.Open(path) checkErr(err) - data, err := ioutil.ReadAll(file) - checkErr(err) - s, err := stream.New(data) + defer file.Close() + s, err := stream.New(file) checkErr(err) sdBlob := &stream.SDBlob{} diff --git a/cmd/start.go b/cmd/start.go index 4d4a56b..7b5facd 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -52,11 +52,13 @@ func init() { } func startCmd(cmd *cobra.Command, args []string) { - db := new(db.SQL) + db := &db.SQL{ + LogQueries: log.GetLevel() == log.DebugLevel, + } err := db.Connect(globalConfig.DBConn) checkErr(err) s3 := store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName) - comboStore := store.NewDBBackedStore(s3, db) + comboStore := store.NewDBBackedStore(s3, db, false) conf := prism.DefaultConf() diff --git a/cmd/test.go b/cmd/test.go index a330856..afa2ff2 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -31,7 +31,7 @@ func testCmd(cmd *cobra.Command, args []string) { memStore := store.NewMemStore() - reflectorServer := reflector.NewServer(memStore) + reflectorServer := reflector.NewServer(memStore, memStore) reflectorServer.Timeout = 3 * time.Minute err := reflectorServer.Start(":" + strconv.Itoa(reflector.DefaultPort)) diff --git a/cmd/upload.go b/cmd/upload.go index 636e824..e2a49cd 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -8,6 +8,7 @@ import ( "github.com/lbryio/reflector.go/db" "github.com/lbryio/reflector.go/reflector" "github.com/lbryio/reflector.go/store" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -30,13 +31,15 @@ func init() { } func uploadCmd(cmd *cobra.Command, args []string) { - db := new(db.SQL) + db := &db.SQL{ + LogQueries: log.GetLevel() == log.DebugLevel, + } err := db.Connect(globalConfig.DBConn) checkErr(err) st := store.NewDBBackedStore( store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName), - db) + db, false) uploader := reflector.NewUploader(db, st, uploadWorkers, uploadSkipExistsCheck, uploadDeleteBlobsAfterUpload) diff --git a/db/db.go b/db/db.go index c3bea24..11f4d53 100644 --- a/db/db.go +++ b/db/db.go @@ -3,16 +3,22 @@ package db import ( "context" "database/sql" + "fmt" + "runtime" + "strings" "time" "github.com/lbryio/lbry.go/v2/dht/bits" "github.com/lbryio/lbry.go/v2/extras/errors" qt "github.com/lbryio/lbry.go/v2/extras/query" + "github.com/lbryio/lbry.go/v2/extras/stop" + "github.com/lbryio/lbry.go/v2/stream" "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql" // blank import for db driver ensures its imported even if its not used log "github.com/sirupsen/logrus" "github.com/volatiletech/null" + "go.uber.org/atomic" ) // SdBlob is a special blob that contains information on the rest of the blobs in the stream @@ -30,19 +36,41 @@ type SdBlob struct { StreamHash string `json:"stream_hash"` } +type trackAccess int + +const ( + // Don't track accesses + TrackAccessNone trackAccess = iota + // Track accesses at the stream level + TrackAccessStreams + // Track accesses at the blob level + TrackAccessBlobs +) + // SQL implements the DB interface type SQL struct { conn *sql.DB - TrackAccessTime bool + // Track the approx last time a blob or stream was accessed + TrackAccess trackAccess + + // Instead of deleting a blob, marked it as not stored in the db + SoftDelete bool + + // Log executed queries. qt.InterpolateParams is cpu-heavy. This avoids that call if not needed. + LogQueries bool } -func logQuery(query string, args ...interface{}) { - s, err := qt.InterpolateParams(query, args...) +func (s SQL) logQuery(query string, args ...interface{}) { + if !s.LogQueries { + return + } + + qStr, err := qt.InterpolateParams(query, args...) if err != nil { log.Errorln(err) } else { - log.Debugln(s) + log.Debugln(qStr) } } @@ -69,43 +97,137 @@ func (s *SQL) AddBlob(hash string, length int, isStored bool) error { return errors.Err("not connected") } - _, err := s.insertBlob(hash, length, isStored) + _, err := s.insertBlob(s.conn, hash, length, isStored) return err } -func (s *SQL) insertBlob(hash string, length int, isStored bool) (int64, error) { +// AddBlob adds a blob to the database. +func (s *SQL) AddBlobs(hash []string) error { + if s.conn == nil { + return errors.Err("not connected") + } + // Split the slice into batches of 20 items. + batch := 10000 + totalBlobs := int64(len(hash)) + work := make(chan []string, 1000) + stopper := stop.New() + var totalInserted atomic.Int64 + start := time.Now() + go func() { + for i := 0; i < len(hash); i += batch { + j := i + batch + if j > len(hash) { + j = len(hash) + } + work <- hash[i:j] + } + log.Infof("done loading %d hashes in the work queue", len(hash)) + close(work) + }() + for i := 0; i < runtime.NumCPU(); i++ { + stopper.Add(1) + go func(worker int) { + log.Infof("starting worker %d", worker) + defer stopper.Done() + for hashes := range work { + inserted := totalInserted.Load() + remaining := totalBlobs - inserted + if inserted > 0 { + timePerBlob := time.Since(start).Microseconds() / inserted + remainingTime := time.Duration(remaining*timePerBlob) * time.Microsecond + log.Infof("[T%d] processing batch of %d items. ETA: %s", worker, len(hashes), remainingTime.String()) + } + err := s.insertBlobs(hashes) // Process the batch. + if err != nil { + log.Errorf("error while inserting batch: %s", errors.FullTrace(err)) + } + totalInserted.Add(int64(len(hashes))) + } + }(i) + } + stopper.Wait() + return nil +} + +func (s *SQL) insertBlobs(hashes []string) error { + var ( + q string + //args []interface{} + ) + dayAgo := time.Now().AddDate(0, 0, -1).Format("2006-01-02 15:04:05") + q = "insert into blob_ (hash, is_stored, length, last_accessed_at) values " + for _, hash := range hashes { + // prepared statements slow everything down by a lot due to reflection + // for this specific instance we'll go ahead and hardcode the query to make it go faster + q += fmt.Sprintf("('%s',1,%d,'%s'),", hash, stream.MaxBlobSize, dayAgo) + //args = append(args, hash, true, stream.MaxBlobSize, dayAgo) + } + q = strings.TrimSuffix(q, ",") + _, err := s.exec(s.conn, q) + if err != nil { + return err + } + + return nil +} + +func (s *SQL) insertBlob(ex Executor, hash string, length int, isStored bool) (int64, error) { if length <= 0 { return 0, errors.Err("length must be positive") } - args := []interface{}{hash, isStored, length} - blobID, err := s.exec( - "INSERT INTO blob_ (hash, is_stored, length) VALUES ("+qt.Qs(len(args))+") ON DUPLICATE KEY UPDATE is_stored = (is_stored or VALUES(is_stored))", - args..., + var ( + q string + args []interface{} ) + if s.TrackAccess == TrackAccessBlobs { + args = []interface{}{hash, isStored, length, time.Now()} + q = "INSERT INTO blob_ (hash, is_stored, length, last_accessed_at) VALUES (" + qt.Qs(len(args)) + ") ON DUPLICATE KEY UPDATE is_stored = (is_stored or VALUES(is_stored)), last_accessed_at = VALUES(last_accessed_at)" + } else { + args = []interface{}{hash, isStored, length} + q = "INSERT INTO blob_ (hash, is_stored, length) VALUES (" + qt.Qs(len(args)) + ") ON DUPLICATE KEY UPDATE is_stored = (is_stored or VALUES(is_stored))" + } + + blobID, err := s.exec(ex, q, args...) if err != nil { return 0, err } if blobID == 0 { - err = s.conn.QueryRow("SELECT id FROM blob_ WHERE hash = ?", hash).Scan(&blobID) + err = ex.QueryRow("SELECT id FROM blob_ WHERE hash = ?", hash).Scan(&blobID) if err != nil { return 0, errors.Err(err) } if blobID == 0 { return 0, errors.Err("blob ID is 0 even after INSERTing and SELECTing") } + + if s.TrackAccess == TrackAccessBlobs { + err := s.touchBlobs(ex, []uint64{uint64(blobID)}) + if err != nil { + return 0, errors.Err(err) + } + } } return blobID, nil } func (s *SQL) insertStream(hash string, sdBlobID int64) (int64, error) { - args := []interface{}{hash, sdBlobID, time.Now()} - streamID, err := s.exec( - "INSERT IGNORE INTO stream (hash, sd_blob_id, last_accessed_at) VALUES ("+qt.Qs(len(args))+")", - args..., + var ( + q string + args []interface{} ) + + if s.TrackAccess == TrackAccessStreams { + args = []interface{}{hash, sdBlobID, time.Now()} + q = "INSERT IGNORE INTO stream (hash, sd_blob_id, last_accessed_at) VALUES (" + qt.Qs(len(args)) + ")" + } else { + args = []interface{}{hash, sdBlobID} + q = "INSERT IGNORE INTO stream (hash, sd_blob_id) VALUES (" + qt.Qs(len(args)) + ")" + } + + streamID, err := s.exec(s.conn, q, args...) if err != nil { return 0, errors.Err(err) } @@ -119,8 +241,8 @@ func (s *SQL) insertStream(hash string, sdBlobID int64) (int64, error) { return 0, errors.Err("stream ID is 0 even after INSERTing and SELECTing") } - if s.TrackAccessTime { - err := s.touch([]uint64{uint64(streamID)}) + if s.TrackAccess == TrackAccessStreams { + err := s.touchStreams([]uint64{uint64(streamID)}) if err != nil { return 0, errors.Err(err) } @@ -130,8 +252,8 @@ func (s *SQL) insertStream(hash string, sdBlobID int64) (int64, error) { } // HasBlob checks if the database contains the blob information. -func (s *SQL) HasBlob(hash string) (bool, error) { - exists, err := s.HasBlobs([]string{hash}) +func (s *SQL) HasBlob(hash string, touch bool) (bool, error) { + exists, err := s.HasBlobs([]string{hash}, touch) if err != nil { return false, err } @@ -139,13 +261,39 @@ func (s *SQL) HasBlob(hash string) (bool, error) { } // HasBlobs checks if the database contains the set of blobs and returns a bool map. -func (s *SQL) HasBlobs(hashes []string) (map[string]bool, error) { - exists, streamsNeedingTouch, err := s.hasBlobs(hashes) - s.touch(streamsNeedingTouch) +func (s *SQL) HasBlobs(hashes []string, touch bool) (map[string]bool, error) { + exists, idsNeedingTouch, err := s.hasBlobs(hashes) + + if touch { + if s.TrackAccess == TrackAccessBlobs { + _ = s.touchBlobs(s.conn, idsNeedingTouch) + } else if s.TrackAccess == TrackAccessStreams { + _ = s.touchStreams(idsNeedingTouch) + } + } + return exists, err } -func (s *SQL) touch(streamIDs []uint64) error { +func (s *SQL) touchBlobs(ex Executor, blobIDs []uint64) error { + if len(blobIDs) == 0 { + return nil + } + + query := "UPDATE blob_ SET last_accessed_at = ? WHERE id IN (" + qt.Qs(len(blobIDs)) + ")" + args := make([]interface{}, len(blobIDs)+1) + args[0] = time.Now() + for i := range blobIDs { + args[i+1] = blobIDs[i] + } + + startTime := time.Now() + _, err := s.exec(ex, query, args...) + log.Debugf("touched %d blobs and took %s", len(blobIDs), time.Since(startTime)) + return errors.Err(err) +} + +func (s *SQL) touchStreams(streamIDs []uint64) error { if len(streamIDs) == 0 { return nil } @@ -158,8 +306,8 @@ func (s *SQL) touch(streamIDs []uint64) error { } startTime := time.Now() - _, err := s.exec(query, args...) - log.Debugf("stream access query touched %d streams and took %s", len(streamIDs), time.Since(startTime)) + _, err := s.exec(s.conn, query, args...) + log.Debugf("touched %d streams and took %s", len(streamIDs), time.Since(startTime)) return errors.Err(err) } @@ -170,14 +318,15 @@ func (s *SQL) hasBlobs(hashes []string) (map[string]bool, []uint64, error) { var ( hash string - streamID uint64 + blobID uint64 + streamID null.Uint64 lastAccessedAt null.Time ) var needsTouch []uint64 exists := make(map[string]bool) - touchDeadline := time.Now().AddDate(0, 0, -1) // touch blob if last accessed before this time + touchDeadline := time.Now().Add(-6 * time.Hour) // touch blob if last accessed before this time maxBatchSize := 10000 doneIndex := 0 @@ -189,20 +338,29 @@ func (s *SQL) hasBlobs(hashes []string) (map[string]bool, []uint64, error) { log.Debugf("getting hashes[%d:%d] of %d", doneIndex, sliceEnd, len(hashes)) batch := hashes[doneIndex:sliceEnd] - // TODO: this query doesn't work for SD blobs, which are not in the stream_blob table - - query := `SELECT b.hash, s.id, s.last_accessed_at + var query string + if s.TrackAccess == TrackAccessBlobs { + query = `SELECT b.hash, b.id, NULL, b.last_accessed_at +FROM blob_ b +WHERE b.is_stored = 1 and b.hash IN (` + qt.Qs(len(batch)) + `)` + } else if s.TrackAccess == TrackAccessStreams { + query = `SELECT b.hash, b.id, s.id, s.last_accessed_at FROM blob_ b LEFT JOIN stream_blob sb ON b.id = sb.blob_id INNER JOIN stream s on (sb.stream_id = s.id or s.sd_blob_id = b.id) -WHERE b.is_stored = ? and b.hash IN (` + qt.Qs(len(batch)) + `)` - args := make([]interface{}, len(batch)+1) - args[0] = true +WHERE b.is_stored = 1 and b.hash IN (` + qt.Qs(len(batch)) + `)` + } else { + query = `SELECT b.hash, b.id, NULL, NULL +FROM blob_ b +WHERE b.is_stored = 1 and b.hash IN (` + qt.Qs(len(batch)) + `)` + } + + args := make([]interface{}, len(batch)) for i := range batch { - args[i+1] = batch[i] + args[i] = batch[i] } - logQuery(query, args...) + s.logQuery(query, args...) err := func() error { startTime := time.Now() @@ -214,13 +372,17 @@ WHERE b.is_stored = ? and b.hash IN (` + qt.Qs(len(batch)) + `)` defer closeRows(rows) for rows.Next() { - err := rows.Scan(&hash, &streamID, &lastAccessedAt) + err := rows.Scan(&hash, &blobID, &streamID, &lastAccessedAt) if err != nil { return errors.Err(err) } exists[hash] = true - if s.TrackAccessTime && (!lastAccessedAt.Valid || lastAccessedAt.Time.Before(touchDeadline)) { - needsTouch = append(needsTouch, streamID) + if !lastAccessedAt.Valid || lastAccessedAt.Time.Before(touchDeadline) { + if s.TrackAccess == TrackAccessBlobs { + needsTouch = append(needsTouch, blobID) + } else if s.TrackAccess == TrackAccessStreams && !streamID.IsZero() { + needsTouch = append(needsTouch, streamID.Uint64) + } } } @@ -240,22 +402,110 @@ WHERE b.is_stored = ? and b.hash IN (` + qt.Qs(len(batch)) + `)` return exists, needsTouch, nil } -// Delete will remove the blob from the db +// Delete will remove (or soft-delete) the blob from the db +// NOTE: If SoftDelete is enabled, streams will never be deleted func (s *SQL) Delete(hash string) error { - _, err := s.exec("DELETE FROM stream WHERE sd_blob_id = (SELECT id FROM blob_ WHERE hash = ?)", hash) + if s.SoftDelete { + _, err := s.exec(s.conn, "UPDATE blob_ SET is_stored = 0 WHERE hash = ?", hash) + return errors.Err(err) + } + + _, err := s.exec(s.conn, "DELETE FROM stream WHERE sd_blob_id = (SELECT id FROM blob_ WHERE hash = ?)", hash) if err != nil { return errors.Err(err) } - _, err = s.exec("DELETE FROM blob_ WHERE hash = ?", hash) + _, err = s.exec(s.conn, "DELETE FROM blob_ WHERE hash = ?", hash) return errors.Err(err) } +// GetHashRange gets the smallest and biggest hashes in the db +func (s *SQL) LeastRecentlyAccessedHashes(maxBlobs int) ([]string, error) { + if s.conn == nil { + return nil, errors.Err("not connected") + } + + if s.TrackAccess != TrackAccessBlobs { + return nil, errors.Err("blob access tracking is disabled") + } + + query := "SELECT hash from blob_ where is_stored = 1 order by last_accessed_at limit ?" + s.logQuery(query, maxBlobs) + + rows, err := s.conn.Query(query, maxBlobs) + if err != nil { + return nil, errors.Err(err) + } + defer closeRows(rows) + + blobs := make([]string, 0, maxBlobs) + for rows.Next() { + var hash string + err := rows.Scan(&hash) + if err != nil { + return nil, errors.Err(err) + } + blobs = append(blobs, hash) + } + + return blobs, nil +} + +// AllHashes writes all hashes from the db into the channel. +// It does not close the channel when it finishes. +//func (s *SQL) AllHashes(ch chan<- string) error { +// if s.conn == nil { +// return errors.Err("not connected") +// } +// +// query := "SELECT hash from blob_" +// if s.SoftDelete { +// query += " where is_stored = 1" +// } +// s.logQuery(query) +// +// rows, err := s.conn.Query(query) +// if err != nil { +// return errors.Err(err) +// } +// defer closeRows(rows) +// +// for rows.Next() { +// var hash string +// err := rows.Scan(&hash) +// if err != nil { +// return errors.Err(err) +// } +// ch <- hash +// // TODO: this needs testing +// // TODO: need a way to cancel this early (e.g. in case of shutdown) +// } +// +// close(ch) +// return nil +//} + +func (s *SQL) Count() (int, error) { + if s.conn == nil { + return 0, errors.Err("not connected") + } + + query := "SELECT count(id) from blob_" + if s.SoftDelete { + query += " where is_stored = 1" + } + s.logQuery(query) + + var count int + err := s.conn.QueryRow(query).Scan(&count) + return count, errors.Err(err) +} + // Block will mark a blob as blocked func (s *SQL) Block(hash string) error { query := "INSERT IGNORE INTO blocked SET hash = ?" args := []interface{}{hash} - logQuery(query, args...) + s.logQuery(query, args...) _, err := s.conn.Exec(query, args...) return errors.Err(err) } @@ -263,7 +513,7 @@ func (s *SQL) Block(hash string) error { // GetBlocked will return a list of blocked hashes func (s *SQL) GetBlocked() (map[string]bool, error) { query := "SELECT hash FROM blocked" - logQuery(query) + s.logQuery(query) rows, err := s.conn.Query(query) if err != nil { return nil, errors.Err(err) @@ -306,7 +556,7 @@ func (s *SQL) MissingBlobsForKnownStream(sdHash string) ([]string, error) { ` args := []interface{}{sdHash} - logQuery(query, args...) + s.logQuery(query, args...) rows, err := s.conn.Query(query, args...) if err != nil { @@ -340,7 +590,7 @@ func (s *SQL) AddSDBlob(sdHash string, sdBlobLength int, sdBlob SdBlob) error { return errors.Err("not connected") } - sdBlobID, err := s.insertBlob(sdHash, sdBlobLength, true) + sdBlobID, err := s.insertBlob(s.conn, sdHash, sdBlobLength, true) if err != nil { return err } @@ -350,28 +600,30 @@ func (s *SQL) AddSDBlob(sdHash string, sdBlobLength int, sdBlob SdBlob) error { return err } - // insert content blobs and connect them to stream - for _, contentBlob := range sdBlob.Blobs { - if contentBlob.BlobHash == "" { - // null terminator blob - continue - } + return withTx(s.conn, func(tx Transactor) error { + // insert content blobs and connect them to stream + for _, contentBlob := range sdBlob.Blobs { + if contentBlob.BlobHash == "" { + // null terminator blob + continue + } - blobID, err := s.insertBlob(contentBlob.BlobHash, contentBlob.Length, false) - if err != nil { - return err - } + blobID, err := s.insertBlob(tx, contentBlob.BlobHash, contentBlob.Length, false) + if err != nil { + return err + } - args := []interface{}{streamID, blobID, contentBlob.BlobNum} - _, err = s.exec( - "INSERT IGNORE INTO stream_blob (stream_id, blob_id, num) VALUES ("+qt.Qs(len(args))+")", - args..., - ) - if err != nil { - return errors.Err(err) + args := []interface{}{streamID, blobID, contentBlob.BlobNum} + _, err = s.exec(tx, + "INSERT IGNORE INTO stream_blob (stream_id, blob_id, num) VALUES ("+qt.Qs(len(args))+")", + args..., + ) + if err != nil { + return errors.Err(err) + } } - } - return nil + return nil + }) } // GetHashRange gets the smallest and biggest hashes in the db @@ -385,7 +637,7 @@ func (s *SQL) GetHashRange() (string, string, error) { query := "SELECT MIN(hash), MAX(hash) from blob_" - logQuery(query) + s.logQuery(query) err := s.conn.QueryRow(query).Scan(&min, &max) return min, max, err @@ -409,7 +661,7 @@ func (s *SQL) GetStoredHashesInRange(ctx context.Context, start, end bits.Bitmap query := "SELECT hash FROM blob_ WHERE hash >= ? AND hash <= ? AND is_stored = 1" args := []interface{}{start.Hex(), end.Hex()} - logQuery(query, args...) + s.logQuery(query, args...) rows, err := s.conn.Query(query, args...) defer closeRows(rows) @@ -444,39 +696,38 @@ func (s *SQL) GetStoredHashesInRange(ctx context.Context, start, end bits.Bitmap } // txFunc is a function that can be wrapped in a transaction -type txFunc func(tx *sql.Tx) error +type txFunc func(tx Transactor) error -// withTx wraps a function in an sql transaction. the transaction is committed if there's no error, or rolled back if there is one. -// if dbOrTx is an sql.DB, a new transaction is started +// withTx wraps a function in an sql transaction. the transaction is committed if there's +// no error, or rolled back if there is one. if dbOrTx is not a Transactor (e.g. if it's +// an *sql.DB), withTx attempts to start a new transaction to use. func withTx(dbOrTx interface{}, f txFunc) (err error) { - var tx *sql.Tx + var tx Transactor + var ok bool - switch t := dbOrTx.(type) { - case *sql.Tx: - tx = t - case *sql.DB: - tx, err = t.Begin() + tx, ok = dbOrTx.(Transactor) + if !ok { + tx, err = Begin(dbOrTx) if err != nil { return err } - defer func() { - if p := recover(); p != nil { - if rollBackError := tx.Rollback(); rollBackError != nil { - log.Error("failed to rollback tx on panic - ", rollBackError) - } - panic(p) - } else if err != nil { - if rollBackError := tx.Rollback(); rollBackError != nil { - log.Error("failed to rollback tx on panic - ", rollBackError) - } - } else { - err = errors.Err(tx.Commit()) - } - }() - default: - return errors.Err("db or tx required") } + defer func() { + if p := recover(); p != nil { + if rollBackError := tx.Rollback(); rollBackError != nil { + log.Error("failed to rollback tx on panic: ", rollBackError) + } + err = errors.Prefix("panic", p) + } else if err != nil { + if rollBackError := tx.Rollback(); rollBackError != nil { + log.Error("failed to rollback tx: ", rollBackError) + } + } else { + err = errors.Err(tx.Commit()) + } + }() + return f(tx) } @@ -489,12 +740,12 @@ func closeRows(rows *sql.Rows) { } } -func (s *SQL) exec(query string, args ...interface{}) (int64, error) { - logQuery(query, args...) +func (s *SQL) exec(ex Executor, query string, args ...interface{}) (int64, error) { + s.logQuery(query, args...) attempt, maxAttempts := 0, 3 Retry: attempt++ - result, err := s.conn.Exec(query, args...) + result, err := ex.Exec(query, args...) if isLockTimeoutError(err) { if attempt <= maxAttempts { //Error 1205: Lock wait timeout exceeded; try restarting transaction @@ -528,8 +779,11 @@ CREATE TABLE blob_ ( hash char(96) NOT NULL, is_stored TINYINT(1) NOT NULL DEFAULT 0, length bigint(20) unsigned DEFAULT NULL, + last_accessed_at TIMESTAMP NULL DEFAULT NULL, PRIMARY KEY (id), - UNIQUE KEY blob_hash_idx (hash) + UNIQUE KEY blob_hash_idx (hash), + KEY `blob_last_accessed_idx` (`last_accessed_at`), + KEY `is_stored_idx` (`is_stored`) ); CREATE TABLE stream ( @@ -560,3 +814,47 @@ CREATE TABLE blocked ( ); */ + +//func (d *LiteDBBackedStore) selfClean() { +// d.stopper.Add(1) +// defer d.stopper.Done() +// lastCleanup := time.Now() +// const cleanupInterval = 10 * time.Second +// for { +// select { +// case <-d.stopper.Ch(): +// log.Infoln("stopping self cleanup") +// return +// default: +// time.Sleep(1 * time.Second) +// } +// if time.Since(lastCleanup) < cleanupInterval { +// continue +// +// blobsCount, err := d.db.BlobsCount() +// if err != nil { +// log.Errorf(errors.FullTrace(err)) +// } +// if blobsCount >= d.maxItems { +// itemsToDelete := blobsCount / 100 * 10 +// blobs, err := d.db.GetLRUBlobs(itemsToDelete) +// if err != nil { +// log.Errorf(errors.FullTrace(err)) +// } +// for _, hash := range blobs { +// select { +// case <-d.stopper.Ch(): +// return +// default: +// +// } +// err = d.Delete(hash) +// if err != nil { +// log.Errorf(errors.FullTrace(err)) +// } +// metrics.CacheLRUEvictCount.With(metrics.CacheLabels(d.Name(), d.component)).Inc() +// } +// } +// lastCleanup = time.Now() +// } +//} diff --git a/db/interfaces.go b/db/interfaces.go new file mode 100644 index 0000000..11b2ee6 --- /dev/null +++ b/db/interfaces.go @@ -0,0 +1,45 @@ +package db + +import ( + "database/sql" + + "github.com/lbryio/lbry.go/v2/extras/errors" +) + +// Executor can perform SQL queries. +type Executor interface { + Exec(query string, args ...interface{}) (sql.Result, error) + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row +} + +// Transactor can commit and rollback, on top of being able to execute queries. +type Transactor interface { + Commit() error + Rollback() error + + Executor +} + +// Begin begins a transaction +func Begin(db interface{}) (Transactor, error) { + type beginner interface { + Begin() (Transactor, error) + } + + creator, ok := db.(beginner) + if ok { + return creator.Begin() + } + + type sqlBeginner interface { + Begin() (*sql.Tx, error) + } + + creator2, ok := db.(sqlBeginner) + if ok { + return creator2.Begin() + } + + return nil, errors.Err("database does not support transactions") +} diff --git a/go.mod b/go.mod index 9df120d..31386d5 100644 --- a/go.mod +++ b/go.mod @@ -2,19 +2,24 @@ module github.com/lbryio/reflector.go replace github.com/btcsuite/btcd => github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19 +//replace github.com/lbryio/lbry.go/v2 => ../lbry.go + require ( github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect github.com/aws/aws-sdk-go v1.16.11 + github.com/bluele/gcache v0.0.2 + github.com/bparli/lfuda-go v0.3.1 github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d + github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 github.com/davecgh/go-spew v1.1.1 + github.com/gin-gonic/gin v1.7.1 github.com/go-sql-driver/mysql v1.4.1 github.com/golang/protobuf v1.4.2 - github.com/google/btree v1.0.0 // indirect - github.com/google/gops v0.3.7 + github.com/google/gops v0.3.18 github.com/gorilla/mux v1.7.4 github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/golang-lru v0.5.4 + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/memberlist v0.1.4 // indirect github.com/hashicorp/serf v0.8.2 github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf @@ -22,25 +27,23 @@ require ( github.com/karrick/godirwalk v1.16.1 github.com/lbryio/chainquery v1.9.0 github.com/lbryio/lbry.go v1.1.2 // indirect - github.com/lbryio/lbry.go/v2 v2.6.1-0.20200901175808-73382bb02128 - github.com/lbryio/types v0.0.0-20191228214437-05a22073b4ec - github.com/lucas-clemente/quic-go v0.18.1 + github.com/lbryio/lbry.go/v2 v2.7.2-0.20210416195322-6516df1418e3 + github.com/lbryio/types v0.0.0-20201019032447-f0b4476ef386 + github.com/lucas-clemente/quic-go v0.20.1 github.com/phayes/freeport v0.0.0-20171002185219-e27662a4a9d6 - github.com/prometheus/client_golang v0.9.2 + github.com/prometheus/client_golang v0.9.3 github.com/sirupsen/logrus v1.4.2 - github.com/spf13/afero v1.4.1 + github.com/spf13/afero v1.4.1 // indirect github.com/spf13/cast v1.3.0 github.com/spf13/cobra v0.0.3 - github.com/spf13/pflag v1.0.3 // indirect - github.com/stretchr/testify v1.4.0 + github.com/spf13/viper v1.7.1 // indirect + github.com/stretchr/testify v1.7.0 github.com/volatiletech/null v8.0.0+incompatible go.uber.org/atomic v1.5.1 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4 // indirect google.golang.org/appengine v1.6.2 // indirect - gotest.tools v2.2.0+incompatible ) go 1.15 diff --git a/go.sum b/go.sum index 84b207a..351f737 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,31 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/StackExchange/wmi v0.0.0-20170410192909-ea383cf3ba6e/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -22,9 +37,15 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.16.11 h1:g/c7gJeVyHoXCxM2fddS85bPGVkBF8s2q8t3fyElegc= github.com/aws/aws-sdk-go v1.16.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= +github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= +github.com/bparli/lfuda-go v0.3.1 h1:nO9Szo627RC8/z+R+MMPBItNwHCOonchmpjQuQi8jVY= +github.com/bparli/lfuda-go v0.3.1/go.mod h1:BR5a9lwlqRqnPhU3F5ojFK3VhTKg8iFVtJJKgZBQhAo= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= @@ -39,61 +60,86 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 h1:t8KYCwSKsOEZBFELI4Pn/phbp38iJ1RRAkDFNin1aak= +github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 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/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8= +github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg= +github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.48.0 h1:TvO60hO/2xgaaTWp2P0wUe4CFxwdMzfbkv3+343Xzqw= github.com/go-ini/ini v1.48.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= 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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 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 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= @@ -107,12 +153,17 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gops v0.3.7 h1:KtVAagOM0FIq+02DiQrKBTnLhYpWBMowaufcj+W1Exw= -github.com/google/gops v0.3.7/go.mod h1:bj0cwMmX1X4XIJFTjR99R5sCxNssNJ8HebFNvoQlmgY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gops v0.3.18 h1:my259V+172PVFmduS2RAsq4FKH+HjKqdh7pLr17Ot8c= +github.com/google/gops v0.3.18/go.mod h1:Pfp8hWGIFdV/7rY9/O/U5WgdjYQXf/GiEK4NVuVd2ZE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f h1:TyqzGm2z1h3AGhjOoRYyeLcW4WlW81MDQkWa+rx/000= @@ -127,13 +178,20 @@ github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -142,6 +200,7 @@ github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -150,12 +209,12 @@ github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= -github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= @@ -163,7 +222,6 @@ github.com/hashicorp/memberlist v0.1.4 h1:gkyML/r71w3FL8gUi74Vk76avkj/9lYAY9lvg0 github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= @@ -179,24 +237,28 @@ github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4 github.com/johntdyer/slackrus v0.0.0-20170926115001-3992f319fd0a/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo= github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY= github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= -github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/keybase/go-ps v0.0.0-20161005175911-668c8856d999/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ= +github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -206,40 +268,43 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lbryio/chainquery v1.9.0 h1:NfBZ3eKYwD3PqXU/vt+2tF3ox3WUWoW4J5YdEQ0rxw0= github.com/lbryio/chainquery v1.9.0/go.mod h1:7G8l7jNtANS1I7fQOvtzbiHsv6qKVmN4codXHc3C4kk= github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c/go.mod h1:muH7wpUqE8hRA3OrYYosw9+Sl681BF9cwcjzE+OCNK8= -github.com/lbryio/lbry.go v1.1.1-0.20190825202001-8fa28d3d656f h1:ovd2wPXzkT80vdP/FX5xcQeXu0i9RAo80SQ6qIsrAjM= github.com/lbryio/lbry.go v1.1.1-0.20190825202001-8fa28d3d656f/go.mod h1:JtyI30bU51rm0LZ/po3mQuzf++14OWb6kR/6mMRAmKU= github.com/lbryio/lbry.go v1.1.2 h1:Dyxc+glT/rVWJwHfIf7vjlPYYbjzrQz5ARmJd5Hp69c= github.com/lbryio/lbry.go v1.1.2/go.mod h1:JtyI30bU51rm0LZ/po3mQuzf++14OWb6kR/6mMRAmKU= -github.com/lbryio/lbry.go/v2 v2.6.1-0.20200901175808-73382bb02128 h1:VL209c+AGKixMFpxT+TsOAPzBPuoUzyjXf47iNe7OzY= -github.com/lbryio/lbry.go/v2 v2.6.1-0.20200901175808-73382bb02128/go.mod h1:RqOv4V5eWY/JGmduCPcQVdN19SEYnNY3SuF+arTKIU4= +github.com/lbryio/lbry.go/v2 v2.7.2-0.20210416195322-6516df1418e3 h1:hkVViG8qbOKJMf/M6mN+Hrtns4j55IU2dRavpJZWbxw= +github.com/lbryio/lbry.go/v2 v2.7.2-0.20210416195322-6516df1418e3/go.mod h1:I1q8W9fwU+t0IWNiprPgE1SorWQwcO6ser0nzP3L5Pk= github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19 h1:/zWD8dVIl7bV1TdJWqPqy9tpqixzX2Qxgit48h3hQcY= github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/lbryio/lbryschema.go v0.0.0-20190428231007-c54836bca002/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo= github.com/lbryio/lbryschema.go v0.0.0-20190602173230-6d2f69a36f46/go.mod h1:dAzPCBj3CKKWBGYBZxK6tKBP5SCgY2tqd9SnQd/OyKo= github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4= github.com/lbryio/types v0.0.0-20190422033210-321fb2abda9c/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE= -github.com/lbryio/types v0.0.0-20191009145016-1bb8107e04f8/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE= -github.com/lbryio/types v0.0.0-20191228214437-05a22073b4ec h1:2xk/qg4VTOCJ8RzV/ED5AKqDcJ00zVb08ltf9V+sr3c= -github.com/lbryio/types v0.0.0-20191228214437-05a22073b4ec/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE= +github.com/lbryio/types v0.0.0-20201019032447-f0b4476ef386 h1:JOQkGpeCM9FWkEHRx+kRPqySPCXElNW1em1++7tVS4M= +github.com/lbryio/types v0.0.0-20201019032447-f0b4476ef386/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lucas-clemente/quic-go v0.18.1 h1:DMR7guC0NtVS8zNZR3IO7NARZvZygkSC56GGtC6cyys= -github.com/lucas-clemente/quic-go v0.18.1/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg= +github.com/lucas-clemente/quic-go v0.20.1 h1:hb5m76V8QS/8Nw/suHvXqo3BMHAozvIkcnzpJdpanSk= +github.com/lucas-clemente/quic-go v0.20.1/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5 h1:mG83tLXWSRdcXMWfkoumVwhcCbf3jHF9QKv/m37BkM0= github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5/go.mod h1:H0aPCWffGOaDcjkw1iB7W9DVLp6GXmfcJY/7YZCWPA4= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marten-seemann/qpack v0.2.0 h1:/r1rhZoOmgxVKBqPNnYilZBDEyw+6OUHCbBzA5jc2y0= -github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc= -github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs= -github.com/marten-seemann/qtls-go1-15 v0.1.0 h1:i/YPXVxz8q9umso/5y474CNcHmTpA+5DH+mFPjx6PZg= -github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A= +github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU= +github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -247,14 +312,21 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= @@ -262,6 +334,7 @@ github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -290,16 +363,26 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rubenv/sql-migrate v0.0.0-20170330050058-38004e7a77f2/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -311,8 +394,7 @@ github.com/sebdah/goldie v0.0.0-20190531093107-d313ffb52c77/go.mod h1:jXP4hmWywN github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sfreiberg/gotwilio v0.0.0-20180612161623-8fb7259ba8bf/go.mod h1:60PiR0SAnAcYSiwrXB6BaxeqHdXMf172toCosHfV+Yk= -github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shirou/gopsutil/v3 v3.21.2/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw= github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20191009025716-f1972eb1d1f5 h1:Gojs/hac/DoYEM7WEICT45+hNWczIeuL5D21e5/HPAw= @@ -346,12 +428,15 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= @@ -359,22 +444,35 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek= +github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d h1:gI4/tqP6lCY5k6Sg+4k9qSoBXmPwG+xXgMpK7jivD4M= @@ -383,15 +481,20 @@ github.com/volatiletech/null v8.0.0+incompatible h1:7wP8m5d/gZ6kW/9GnrLtMCRre2dl github.com/volatiletech/null v8.0.0+incompatible/go.mod h1:0wD98JzdqB+rLyZ70fN05VDbXbafIb0KU0MdVhCzmOQ= github.com/volatiletech/sqlboiler v3.4.0+incompatible h1:saQ6WxZ9wEJp33q3w/DHs7an7SYi1H7Yzf4/moxCbJU= github.com/volatiletech/sqlboiler v3.4.0+incompatible/go.mod h1:jLfDkkHWPbS2cWRLkyC20vQWaIQsASEY7gM7zSo11Yw= -github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg= github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM= go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -400,6 +503,7 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -409,14 +513,28 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/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= @@ -425,15 +543,17 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/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-20190108225652-1e06a53dbb7e/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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko= golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= @@ -442,61 +562,78 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20171017063910-8dbc5d05d6ed/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190520201301-c432e742b0af/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU= +golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w= golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/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-20181030000716-a0a13e073c7b/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4 h1:Toz2IK7k8rbltAXwNAxKcn9OzqyNfMUhUNjz3sL0NMk= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -507,10 +644,17 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2 h1:j8RI1yW0SkI+paT6uGwMlrMI/6zwYA6/CFil8rxOzGI= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -519,13 +663,21 @@ google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= 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= @@ -534,36 +686,43 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +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/gorp.v1 v1.7.1/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.48.0 h1:URjZc+8ugRY5mL5uUeQH/a63JcHwdX9xZaWvmNWD7z8= gopkg.in/ini.v1 v1.48.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79 h1:FpCr9V8wuOei4BAen+93HtVJ+XSi+KPbaPKm0Vj5R64= gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79/go.mod h1:gWkaRU7CoXpezCBWfWjm3999QqS+1pYPXGbqQCTMzo8= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -rsc.io/goversion v1.0.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 8d46266..b6d5a57 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -60,6 +60,7 @@ func (s *Server) Shutdown() { const ( ns = "reflector" subsystemCache = "cache" + subsystemITTT = "ittt" labelDirection = "direction" labelErrorType = "error_type" @@ -117,6 +118,11 @@ var ( Name: "http3_blob_download_total", Help: "Total number of blobs downloaded from reflector through QUIC protocol", }) + HttpDownloadCount = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: ns, + Name: "http_blob_download_total", + Help: "Total number of blobs downloaded from reflector through HTTP protocol", + }) CacheHitCount = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, @@ -124,6 +130,18 @@ var ( Name: "hit_total", Help: "Total number of blobs retrieved from the cache storage", }, []string{LabelCacheType, LabelComponent}) + ThisHitCount = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: ns, + Subsystem: subsystemITTT, + Name: "this_hit_total", + Help: "Total number of blobs retrieved from the this storage", + }) + ThatHitCount = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: ns, + Subsystem: subsystemITTT, + Name: "that_hit_total", + Help: "Total number of blobs retrieved from the that storage", + }) CacheMissCount = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: subsystemCache, @@ -181,11 +199,21 @@ var ( Name: "udp_in_bytes", Help: "Total number of bytes downloaded through UDP", }) + MtrInBytesHttp = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: ns, + Name: "http_in_bytes", + Help: "Total number of bytes downloaded through HTTP", + }) MtrOutBytesUdp = promauto.NewCounter(prometheus.CounterOpts{ Namespace: ns, Name: "udp_out_bytes", Help: "Total number of bytes streamed out through UDP", }) + MtrOutBytesHttp = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: ns, + Name: "http_out_bytes", + Help: "Total number of bytes streamed out through UDP", + }) MtrInBytesReflector = promauto.NewCounter(prometheus.CounterOpts{ Namespace: ns, Name: "reflector_in_bytes", @@ -201,6 +229,16 @@ var ( Name: "s3_in_bytes", Help: "Total number of incoming bytes (from S3-CF)", }) + Http3BlobReqQueue = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: ns, + Name: "http3_blob_request_queue_size", + Help: "Blob requests of https queue size", + }) + RoutinesQueue = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Name: "routines", + Help: "routines running by type", + }, []string{"package", "kind"}) ) func CacheLabels(name, component string) prometheus.Labels { diff --git a/peer/client.go b/peer/client.go index cdafd1c..9eae088 100644 --- a/peer/client.go +++ b/peer/client.go @@ -9,6 +9,7 @@ import ( "time" "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" "github.com/lbryio/reflector.go/store" "github.com/lbryio/lbry.go/v2/extras/errors" @@ -57,10 +58,11 @@ func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Str var sd stream.SDBlob - b, err := c.GetBlob(sdHash) + b, trace, err := c.GetBlob(sdHash) if err != nil { return nil, err } + log.Debug(trace.String()) err = sd.FromBlob(b) if err != nil { @@ -71,10 +73,11 @@ func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Str s[0] = b for i := 0; i < len(sd.BlobInfos)-1; i++ { - s[i+1], err = c.GetBlob(hex.EncodeToString(sd.BlobInfos[i].BlobHash)) + s[i+1], trace, err = c.GetBlob(hex.EncodeToString(sd.BlobInfos[i].BlobHash)) if err != nil { return nil, err } + log.Debug(trace.String()) } return s, nil @@ -114,47 +117,52 @@ func (c *Client) HasBlob(hash string) (bool, error) { } // GetBlob gets a blob -func (c *Client) GetBlob(hash string) (stream.Blob, error) { +func (c *Client) GetBlob(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() if !c.connected { - return nil, errors.Err("not connected") + return nil, shared.NewBlobTrace(time.Since(start), "tcp"), errors.Err("not connected") } sendRequest, err := json.Marshal(blobRequest{ RequestedBlob: hash, }) if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), "tcp"), err } err = c.write(sendRequest) if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), "tcp"), err } var resp blobResponse err = c.read(&resp) if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), "tcp"), err } + trace := shared.NewBlobTrace(time.Since(start), "tcp") + if resp.RequestTrace != nil { + trace = *resp.RequestTrace + } if resp.IncomingBlob.Error != "" { - return nil, errors.Prefix(hash[:8], resp.IncomingBlob.Error) + return nil, trace, errors.Prefix(hash[:8], resp.IncomingBlob.Error) } if resp.IncomingBlob.BlobHash != hash { - return nil, errors.Prefix(hash[:8], "blob hash in response does not match requested hash") + return nil, trace.Stack(time.Since(start), "tcp"), errors.Prefix(hash[:8], "blob hash in response does not match requested hash") } if resp.IncomingBlob.Length <= 0 { - return nil, errors.Prefix(hash[:8], "length reported as <= 0") + return nil, trace, errors.Prefix(hash[:8], "length reported as <= 0") } log.Debugf("receiving blob %s from %s", hash[:8], c.conn.RemoteAddr()) blob, err := c.readRawBlob(resp.IncomingBlob.Length) if err != nil { - return nil, err + return nil, (*resp.RequestTrace).Stack(time.Since(start), "tcp"), err } metrics.MtrInBytesTcp.Add(float64(len(blob))) - return blob, nil + return blob, trace.Stack(time.Since(start), "tcp"), nil } func (c *Client) read(v interface{}) error { diff --git a/peer/http3/client.go b/peer/http3/client.go index ad5c741..d9b1495 100644 --- a/peer/http3/client.go +++ b/peer/http3/client.go @@ -9,12 +9,14 @@ import ( "sync" "time" - "github.com/lbryio/lbry.go/v2/extras/errors" - "github.com/lbryio/lbry.go/v2/stream" "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" "github.com/lbryio/reflector.go/store" + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/lbry.go/v2/stream" "github.com/lucas-clemente/quic-go/http3" + log "github.com/sirupsen/logrus" ) // Client is an instance of a client connected to a server. @@ -35,7 +37,7 @@ func (c *Client) Close() error { func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Stream, error) { var sd stream.SDBlob - b, err := c.GetBlob(sdHash) + b, _, err := c.GetBlob(sdHash) if err != nil { return nil, err } @@ -49,10 +51,12 @@ func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Str s[0] = b for i := 0; i < len(sd.BlobInfos)-1; i++ { - s[i+1], err = c.GetBlob(hex.EncodeToString(sd.BlobInfos[i].BlobHash)) + var trace shared.BlobTrace + s[i+1], trace, err = c.GetBlob(hex.EncodeToString(sd.BlobInfos[i].BlobHash)) if err != nil { return nil, err } + log.Debug(trace.String()) } return s, nil @@ -75,26 +79,35 @@ func (c *Client) HasBlob(hash string) (bool, error) { } // GetBlob gets a blob -func (c *Client) GetBlob(hash string) (stream.Blob, error) { - resp, err := c.conn.Get(fmt.Sprintf("https://%s/get/%s", c.ServerAddr, hash)) +func (c *Client) GetBlob(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + resp, err := c.conn.Get(fmt.Sprintf("https://%s/get/%s?trace=true", c.ServerAddr, hash)) if err != nil { - return nil, errors.Err(err) + return nil, shared.NewBlobTrace(time.Since(start), "http3"), errors.Err(err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { fmt.Printf("%s blob not found %d\n", hash, resp.StatusCode) - return nil, errors.Err(store.ErrBlobNotFound) + return nil, shared.NewBlobTrace(time.Since(start), "http3"), errors.Err(store.ErrBlobNotFound) } else if resp.StatusCode != http.StatusOK { - return nil, errors.Err("non 200 status code returned: %d", resp.StatusCode) + return nil, shared.NewBlobTrace(time.Since(start), "http3"), errors.Err("non 200 status code returned: %d", resp.StatusCode) } tmp := getBuffer() defer putBuffer(tmp) - + serialized := resp.Header.Get("Via") + trace := shared.NewBlobTrace(time.Since(start), "http3") + if serialized != "" { + parsedTrace, err := shared.Deserialize(serialized) + if err != nil { + return nil, shared.NewBlobTrace(time.Since(start), "http3"), err + } + trace = *parsedTrace + } written, err := io.Copy(tmp, resp.Body) if err != nil { - return nil, errors.Err(err) + return nil, trace.Stack(time.Since(start), "http3"), errors.Err(err) } blob := make([]byte, written) @@ -102,7 +115,7 @@ func (c *Client) GetBlob(hash string) (stream.Blob, error) { metrics.MtrInBytesUdp.Add(float64(len(blob))) - return blob, nil + return blob, trace.Stack(time.Since(start), "http3"), nil } // buffer pool to reduce GC diff --git a/peer/http3/server.go b/peer/http3/server.go index 55ab5fc..3d49d17 100644 --- a/peer/http3/server.go +++ b/peer/http3/server.go @@ -10,6 +10,8 @@ import ( "fmt" "math/big" "net/http" + "strconv" + "sync" "time" "github.com/lbryio/reflector.go/internal/metrics" @@ -31,7 +33,7 @@ type Server struct { } // NewServer returns an initialized Server pointer. -func NewServer(store store.BlobStore) *Server { +func NewServer(store store.BlobStore, requestQueueSize int) *Server { return &Server{ store: store, grp: stop.New(), @@ -63,33 +65,21 @@ type availabilityResponse struct { // Start starts the server listener to handle connections. func (s *Server) Start(address string) error { log.Println("HTTP3 peer listening on " + address) + window500M := 500 * 1 << 20 + quicConf := &quic.Config{ - HandshakeTimeout: 4 * time.Second, - MaxIdleTimeout: 10 * time.Second, + MaxStreamReceiveWindow: uint64(window500M), + MaxConnectionReceiveWindow: uint64(window500M), + EnableDatagrams: true, + HandshakeIdleTimeout: 4 * time.Second, + MaxIdleTimeout: 20 * time.Second, } r := mux.NewRouter() r.HandleFunc("/get/{hash}", func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - requestedBlob := vars["hash"] - blob, err := s.store.Get(requestedBlob) - if err != nil { - if errors.Is(err, store.ErrBlobNotFound) { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - fmt.Printf("%s: %s", requestedBlob, errors.FullTrace(err)) - s.logError(err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - _, err = w.Write(blob) - if err != nil { - s.logError(err) - } - metrics.MtrOutBytesUdp.Add(float64(len(blob))) - metrics.BlobDownloadCount.Inc() - metrics.Http3DownloadCount.Inc() + waiter := &sync.WaitGroup{} + waiter.Add(1) + enqueue(&blobRequest{request: r, reply: w, finished: waiter}) + waiter.Wait() }) r.HandleFunc("/has/{hash}", func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -127,7 +117,7 @@ func (s *Server) Start(address string) error { }, QuicConfig: quicConf, } - + go InitWorkers(s, 200) go s.listenForShutdown(&server) s.grp.Add(1) go func() { @@ -176,3 +166,47 @@ func (s *Server) listenForShutdown(listener *http3.Server) { log.Error("error closing listener for peer server - ", err) } } + +func (s *Server) HandleGetBlob(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + requestedBlob := vars["hash"] + traceParam := r.URL.Query().Get("trace") + var err error + wantsTrace := false + if traceParam != "" { + wantsTrace, err = strconv.ParseBool(traceParam) + if err != nil { + wantsTrace = false + } + } + + blob, trace, err := s.store.Get(requestedBlob) + + if wantsTrace { + serialized, err := trace.Serialize() + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + w.Header().Add("Via", serialized) + log.Debug(trace.String()) + } + if err != nil { + if errors.Is(err, store.ErrBlobNotFound) { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + fmt.Printf("%s: %s", requestedBlob, errors.FullTrace(err)) + s.logError(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err = w.Write(blob) + if err != nil { + s.logError(err) + } + metrics.MtrOutBytesUdp.Add(float64(len(blob))) + metrics.BlobDownloadCount.Inc() + metrics.Http3DownloadCount.Inc() +} diff --git a/peer/http3/store.go b/peer/http3/store.go index 4749ab0..7d8fc34 100644 --- a/peer/http3/store.go +++ b/peer/http3/store.go @@ -4,10 +4,14 @@ import ( "crypto/tls" "crypto/x509" "net/http" + "strings" + "sync" "time" "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" + "github.com/lbryio/reflector.go/store" "github.com/lucas-clemente/quic-go" "github.com/lucas-clemente/quic-go/http3" ) @@ -15,7 +19,8 @@ import ( // Store is a blob store that gets blobs from a peer. // It satisfies the store.BlobStore interface but cannot put or delete blobs. type Store struct { - opts StoreOpts + opts StoreOpts + NotFoundCache *sync.Map } // StoreOpts allows to set options for a new Store. @@ -26,13 +31,17 @@ type StoreOpts struct { // NewStore makes a new peer store. func NewStore(opts StoreOpts) *Store { - return &Store{opts: opts} + return &Store{opts: opts, NotFoundCache: &sync.Map{}} } func (p *Store) getClient() (*Client, error) { var qconf quic.Config - qconf.HandshakeTimeout = 4 * time.Second - qconf.MaxIdleTimeout = 10 * time.Second + window500M := 500 * 1 << 20 + qconf.MaxStreamReceiveWindow = uint64(window500M) + qconf.MaxConnectionReceiveWindow = uint64(window500M) + qconf.EnableDatagrams = true + qconf.HandshakeIdleTimeout = 4 * time.Second + qconf.MaxIdleTimeout = 20 * time.Second pool, err := x509.SystemCertPool() if err != nil { return nil, err @@ -68,10 +77,19 @@ func (p *Store) Has(hash string) (bool, error) { } // Get downloads the blob from the peer -func (p *Store) Get(hash string) (stream.Blob, error) { +func (p *Store) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + if lastChecked, ok := p.NotFoundCache.Load(hash); ok { + if lastChecked.(time.Time).After(time.Now().Add(-5 * time.Minute)) { + return nil, shared.NewBlobTrace(time.Since(start), p.Name()+"-notfoundcache"), store.ErrBlobNotFound + } + } c, err := p.getClient() + if err != nil && strings.Contains(err.Error(), "blob not found") { + p.NotFoundCache.Store(hash, time.Now()) + } if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), p.Name()), err } defer c.Close() return c.GetBlob(hash) @@ -79,15 +97,20 @@ func (p *Store) Get(hash string) (stream.Blob, error) { // Put is not supported func (p *Store) Put(hash string, blob stream.Blob) error { - panic("http3Store cannot put or delete blobs") + return errors.Err(shared.ErrNotImplemented) } // PutSD is not supported func (p *Store) PutSD(hash string, blob stream.Blob) error { - panic("http3Store cannot put or delete blobs") + return errors.Err(shared.ErrNotImplemented) } // Delete is not supported func (p *Store) Delete(hash string) error { - panic("http3Store cannot put or delete blobs") + return errors.Err(shared.ErrNotImplemented) +} + +// Delete is not supported +func (p *Store) Shutdown() { + return } diff --git a/peer/http3/worker.go b/peer/http3/worker.go new file mode 100644 index 0000000..91dfd02 --- /dev/null +++ b/peer/http3/worker.go @@ -0,0 +1,47 @@ +package http3 + +import ( + "net/http" + "sync" + + "github.com/lbryio/reflector.go/internal/metrics" + + "github.com/lbryio/lbry.go/v2/extras/stop" +) + +type blobRequest struct { + request *http.Request + reply http.ResponseWriter + finished *sync.WaitGroup +} + +var getReqCh = make(chan *blobRequest, 20000) + +func InitWorkers(server *Server, workers int) { + stopper := stop.New(server.grp) + for i := 0; i < workers; i++ { + metrics.RoutinesQueue.WithLabelValues("http3", "worker").Inc() + go func(worker int) { + defer metrics.RoutinesQueue.WithLabelValues("http3", "worker").Dec() + for { + select { + case <-stopper.Ch(): + case r := <-getReqCh: + metrics.Http3BlobReqQueue.Dec() + process(server, r) + } + } + }(i) + } + return +} + +func enqueue(b *blobRequest) { + metrics.Http3BlobReqQueue.Inc() + getReqCh <- b +} + +func process(server *Server, r *blobRequest) { + server.HandleGetBlob(r.reply, r.request) + r.finished.Done() +} diff --git a/peer/server.go b/peer/server.go index 44916cb..4061ea5 100644 --- a/peer/server.go +++ b/peer/server.go @@ -12,6 +12,7 @@ import ( "github.com/lbryio/reflector.go/internal/metrics" "github.com/lbryio/reflector.go/reflector" + "github.com/lbryio/reflector.go/shared" "github.com/lbryio/reflector.go/store" "github.com/lbryio/lbry.go/v2/extras/errors" @@ -88,7 +89,9 @@ func (s *Server) listenAndServe(listener net.Listener) { log.Error(errors.Prefix("accepting conn", err)) } else { s.grp.Add(1) + metrics.RoutinesQueue.WithLabelValues("peer", "server-handleconn").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("peer", "server-handleconn").Dec() s.handleConnection(conn) s.grp.Done() }() @@ -253,6 +256,7 @@ func (s *Server) handleCompositeRequest(data []byte) ([]byte, error) { } var blob []byte + var trace shared.BlobTrace if request.RequestedBlob != "" { if len(request.RequestedBlob) != stream.BlobHashHexLength { return nil, errors.Err("Invalid blob hash length") @@ -260,7 +264,8 @@ func (s *Server) handleCompositeRequest(data []byte) ([]byte, error) { log.Debugln("Sending blob " + request.RequestedBlob[:8]) - blob, err = s.store.Get(request.RequestedBlob) + blob, trace, err = s.store.Get(request.RequestedBlob) + log.Debug(trace.String()) if errors.Is(err, store.ErrBlobNotFound) { response.IncomingBlob = incomingBlob{ Error: err.Error(), @@ -382,6 +387,7 @@ type incomingBlob struct { } type blobResponse struct { IncomingBlob incomingBlob `json:"incoming_blob"` + RequestTrace *shared.BlobTrace } type compositeRequest struct { diff --git a/peer/store.go b/peer/store.go index b8abedd..689d1c0 100644 --- a/peer/store.go +++ b/peer/store.go @@ -1,10 +1,13 @@ package peer import ( + "strings" "time" "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" + "github.com/lbryio/reflector.go/store" ) // Store is a blob store that gets blobs from a peer. @@ -43,26 +46,37 @@ func (p *Store) Has(hash string) (bool, error) { } // Get downloads the blob from the peer -func (p *Store) Get(hash string) (stream.Blob, error) { +func (p *Store) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() c, err := p.getClient() if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), p.Name()), err } defer c.Close() - return c.GetBlob(hash) + blob, trace, err := c.GetBlob(hash) + if err != nil && strings.Contains(err.Error(), "blob not found") { + return nil, trace, store.ErrBlobNotFound + } + + return blob, trace, err } // Put is not supported func (p *Store) Put(hash string, blob stream.Blob) error { - panic("PeerStore cannot put or delete blobs") + return errors.Err(shared.ErrNotImplemented) } // PutSD is not supported func (p *Store) PutSD(hash string, blob stream.Blob) error { - panic("PeerStore cannot put or delete blobs") + return errors.Err(shared.ErrNotImplemented) } // Delete is not supported func (p *Store) Delete(hash string) error { - panic("PeerStore cannot put or delete blobs") + return errors.Err(shared.ErrNotImplemented) +} + +// Delete is not supported +func (p *Store) Shutdown() { + return } diff --git a/prism/prism.go b/prism/prism.go index 12d0329..ee18ba1 100644 --- a/prism/prism.go +++ b/prism/prism.go @@ -79,7 +79,7 @@ func New(conf *Config) *Prism { dht: d, cluster: c, peer: peer.NewServer(conf.Blobs), - reflector: reflector.NewServer(conf.Blobs), + reflector: reflector.NewServer(conf.Blobs, conf.Blobs), grp: stop.New(), } diff --git a/publish/publish.go b/publish/publish.go index 417978e..9e585e8 100644 --- a/publish/publish.go +++ b/publish/publish.go @@ -3,7 +3,6 @@ package publish import ( "bytes" "encoding/json" - "io/ioutil" "os" "path/filepath" "sort" @@ -21,7 +20,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/golang/protobuf/proto" - "golang.org/x/crypto/sha3" ) var TODO = ` @@ -43,6 +41,14 @@ var TODO = ` } ` +type Details struct { + Title string + Description string + Author string + Tags []string + ReleaseTime int64 +} + func Publish(client *lbrycrd.Client, path, name, address string, details Details, reflectorAddress string) (*wire.MsgTx, *chainhash.Hash, error) { if name == "" { return nil, nil, errors.Err("name required") @@ -69,11 +75,20 @@ func Publish(client *lbrycrd.Client, path, name, address string, details Details return nil, nil, err } - claim, st, err := makeClaimAndStream(path, details) + st, stPB, err := makeStream(path) if err != nil { return nil, nil, err } + stPB.Author = details.Author + stPB.ReleaseTime = details.ReleaseTime + + claim := &pb.Claim{ + Title: details.Title, + Description: details.Description, + Type: &pb.Claim_Stream{Stream: stPB}, + } + err = addClaimToTx(tx, claim, name, amount, addr) if err != nil { return nil, nil, err @@ -203,50 +218,31 @@ func reflect(st stream.Stream, reflectorAddress string) error { return nil } -type Details struct { - Title string - Description string - Author string - Tags []string - ReleaseTime int64 -} - -func makeClaimAndStream(path string, details Details) (*pb.Claim, stream.Stream, error) { +func makeStream(path string) (stream.Stream, *pb.Stream, error) { file, err := os.Open(path) if err != nil { return nil, nil, errors.Err(err) } - data, err := ioutil.ReadAll(file) - if err != nil { - return nil, nil, errors.Err(err) - } - s, err := stream.New(data) - if err != nil { - return nil, nil, errors.Err(err) - } + defer file.Close() + + enc := stream.NewEncoder(file) - // make the claim - sdBlob := &stream.SDBlob{} - err = sdBlob.FromBlob(s[0]) + s, err := enc.Stream() if err != nil { return nil, nil, errors.Err(err) } - filehash := sha3.Sum384(data) - - streamPB := &pb.Stream{ - Author: details.Author, - ReleaseTime: details.ReleaseTime, + streamProto := &pb.Stream{ Source: &pb.Source{ - SdHash: s[0].Hash(), + SdHash: enc.SDBlob().Hash(), Name: filepath.Base(file.Name()), - Size: uint64(len(data)), - Hash: filehash[:], + Size: uint64(enc.SourceLen()), + Hash: enc.SourceHash(), }, } mimeType, category := guessMimeType(filepath.Ext(file.Name())) - streamPB.Source.MediaType = mimeType + streamProto.Source.MediaType = mimeType switch category { case "video": @@ -254,20 +250,14 @@ func makeClaimAndStream(path string, details Details) (*pb.Claim, stream.Stream, //if err != nil { // return nil, nil, err //} - streamPB.Type = &pb.Stream_Video{} + streamProto.Type = &pb.Stream_Video{} case "audio": - streamPB.Type = &pb.Stream_Audio{} + streamProto.Type = &pb.Stream_Audio{} case "image": - streamPB.Type = &pb.Stream_Image{} - } - - claim := &pb.Claim{ - Title: details.Title, - Description: details.Description, - Type: &pb.Claim_Stream{Stream: streamPB}, + streamProto.Type = &pb.Stream_Image{} } - return claim, s, nil + return s, streamProto, nil } func getClaimPayoutScript(name string, value []byte, address btcutil.Address) ([]byte, error) { diff --git a/reflector/blocklist.go b/reflector/blocklist.go index 9abfc4a..26edff6 100644 --- a/reflector/blocklist.go +++ b/reflector/blocklist.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/store" "github.com/lbryio/reflector.go/wallet" @@ -109,8 +111,9 @@ func sdHashesForOutpoints(walletServers, outpoints []string, stopper stop.Chan) } done := make(chan bool) - + metrics.RoutinesQueue.WithLabelValues("reflector", "sdhashesforoutput").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "sdhashesforoutput").Dec() select { case <-done: case <-stopper: diff --git a/reflector/server.go b/reflector/server.go index bbe3d33..3171ced 100644 --- a/reflector/server.go +++ b/reflector/server.go @@ -10,6 +10,7 @@ import ( "net" "time" + "github.com/google/gops/agent" "github.com/lbryio/reflector.go/internal/metrics" "github.com/lbryio/reflector.go/store" @@ -40,16 +41,18 @@ type Server struct { EnableBlocklist bool // if true, blocklist checking and blob deletion will be enabled - store store.BlobStore - grp *stop.Group + underlyingStore store.BlobStore + outerStore store.BlobStore + grp *stop.Group } // NewServer returns an initialized reflector server pointer. -func NewServer(store store.BlobStore) *Server { +func NewServer(underlying store.BlobStore, outer store.BlobStore) *Server { return &Server{ - Timeout: DefaultTimeout, - store: store, - grp: stop.New(), + Timeout: DefaultTimeout, + underlyingStore: underlying, + outerStore: outer, + grp: stop.New(), } } @@ -67,9 +70,13 @@ func (s *Server) Start(address string) error { return errors.Err(err) } log.Println("reflector listening on " + address) - + if err := agent.Listen(agent.Options{}); err != nil { + log.Fatal(err) + } s.grp.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "listener").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "listener").Dec() <-s.grp.Ch() err := l.Close() if err != nil { @@ -79,15 +86,19 @@ func (s *Server) Start(address string) error { }() s.grp.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "start").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "start").Dec() s.listenAndServe(l) s.grp.Done() }() if s.EnableBlocklist { - if b, ok := s.store.(store.Blocklister); ok { + if b, ok := s.underlyingStore.(store.Blocklister); ok { s.grp.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "enableblocklist").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "enableblocklist").Dec() s.enableBlocklist(b) s.grp.Done() }() @@ -110,7 +121,9 @@ func (s *Server) listenAndServe(listener net.Listener) { log.Error(err) } else { s.grp.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "server-listenandserve").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "server-listenandserve").Inc() s.handleConn(conn) s.grp.Done() }() @@ -125,7 +138,9 @@ func (s *Server) handleConn(conn net.Conn) { close(connNeedsClosing) }() s.grp.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "server-handleconn").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "server-handleconn").Dec() defer s.grp.Done() select { case <-connNeedsClosing: @@ -190,13 +205,13 @@ func (s *Server) receiveBlob(conn net.Conn) error { } var wantsBlob bool - if bl, ok := s.store.(store.Blocklister); ok { + if bl, ok := s.underlyingStore.(store.Blocklister); ok { wantsBlob, err = bl.Wants(blobHash) if err != nil { return err } } else { - blobExists, err := s.store.Has(blobHash) + blobExists, err := s.underlyingStore.Has(blobHash) if err != nil { return err } @@ -206,7 +221,7 @@ func (s *Server) receiveBlob(conn net.Conn) error { var neededBlobs []string if isSdBlob && !wantsBlob { - if nbc, ok := s.store.(neededBlobChecker); ok { + if nbc, ok := s.underlyingStore.(neededBlobChecker); ok { neededBlobs, err = nbc.MissingBlobsForKnownStream(blobHash) if err != nil { return err @@ -249,9 +264,9 @@ func (s *Server) receiveBlob(conn net.Conn) error { log.Debugln("Got blob " + blobHash[:8]) if isSdBlob { - err = s.store.PutSD(blobHash, blob) + err = s.outerStore.PutSD(blobHash, blob) } else { - err = s.store.Put(blobHash, blob) + err = s.outerStore.Put(blobHash, blob) } if err != nil { return err diff --git a/reflector/server_test.go b/reflector/server_test.go index 0de200e..9004d84 100644 --- a/reflector/server_test.go +++ b/reflector/server_test.go @@ -22,7 +22,7 @@ func startServerOnRandomPort(t *testing.T) (*Server, int) { t.Fatal(err) } - srv := NewServer(store.NewMemStore()) + srv := NewServer(store.NewMemStore(), store.NewMemStore()) err = srv.Start("127.0.0.1:" + strconv.Itoa(port)) if err != nil { t.Fatal(err) @@ -119,7 +119,7 @@ func TestServer_Timeout(t *testing.T) { t.Fatal(err) } - srv := NewServer(store.NewMemStore()) + srv := NewServer(store.NewMemStore(), store.NewMemStore()) srv.Timeout = testTimeout err = srv.Start("127.0.0.1:" + strconv.Itoa(port)) if err != nil { @@ -190,7 +190,7 @@ func TestServer_PartialUpload(t *testing.T) { t.Fatal(err) } - srv := NewServer(st) + srv := NewServer(st, st) err = srv.Start("127.0.0.1:" + strconv.Itoa(port)) if err != nil { t.Fatal(err) diff --git a/reflector/uploader.go b/reflector/uploader.go index ae762f4..b421272 100644 --- a/reflector/uploader.go +++ b/reflector/uploader.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/db" "github.com/lbryio/reflector.go/store" @@ -74,7 +76,7 @@ func (u *Uploader) Upload(dirOrFilePath string) error { var exists map[string]bool if !u.skipExistsCheck { - exists, err = u.db.HasBlobs(hashes) + exists, err = u.db.HasBlobs(hashes, false) if err != nil { return err } @@ -88,7 +90,9 @@ func (u *Uploader) Upload(dirOrFilePath string) error { for i := 0; i < u.workers; i++ { workerWG.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "upload").Inc() go func(i int) { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "upload").Dec() defer workerWG.Done() defer func(i int) { log.Debugf("worker %d quitting", i) }(i) u.worker(pathChan) @@ -97,7 +101,9 @@ func (u *Uploader) Upload(dirOrFilePath string) error { countWG := sync.WaitGroup{} countWG.Add(1) + metrics.RoutinesQueue.WithLabelValues("reflector", "uploader").Inc() go func() { + defer metrics.RoutinesQueue.WithLabelValues("reflector", "uploader").Dec() defer countWG.Done() u.counter() }() diff --git a/server/http/routes.go b/server/http/routes.go new file mode 100644 index 0000000..e9124a9 --- /dev/null +++ b/server/http/routes.go @@ -0,0 +1,59 @@ +package http + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/lbryio/lbry.go/v2/extras/errors" + + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/store" + + log "github.com/sirupsen/logrus" +) + +func (s *Server) getBlob(c *gin.Context) { + hash := c.Query("hash") + blob, trace, err := s.store.Get(hash) + if err != nil { + serialized, serializeErr := trace.Serialize() + if serializeErr != nil { + _ = c.AbortWithError(http.StatusInternalServerError, errors.Prefix(serializeErr.Error(), err)) + return + } + c.Header("Via", serialized) + + if errors.Is(err, store.ErrBlobNotFound) { + log.Errorf("wtf: %s", err.Error()) + c.AbortWithStatus(http.StatusNotFound) + return + } + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + serialized, err := trace.Serialize() + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + metrics.MtrOutBytesHttp.Add(float64(len(blob))) + metrics.BlobDownloadCount.Inc() + metrics.HttpDownloadCount.Inc() + c.Header("Via", serialized) + c.Header("Content-Disposition", "filename="+hash) + c.Data(http.StatusOK, "application/octet-stream", blob) +} + +func (s *Server) hasBlob(c *gin.Context) { + hash := c.Query("hash") + has, err := s.store.Has(hash) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + if has { + c.Status(http.StatusNoContent) + return + } + c.Status(http.StatusNotFound) +} diff --git a/server/http/server.go b/server/http/server.go new file mode 100644 index 0000000..adc104f --- /dev/null +++ b/server/http/server.go @@ -0,0 +1,68 @@ +package http + +import ( + "context" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/lbryio/lbry.go/v2/extras/stop" + "github.com/lbryio/reflector.go/store" + log "github.com/sirupsen/logrus" +) + +// Server is an instance of a peer server that houses the listener and store. +type Server struct { + store store.BlobStore + grp *stop.Group +} + +// NewServer returns an initialized Server pointer. +func NewServer(store store.BlobStore) *Server { + return &Server{ + store: store, + grp: stop.New(), + } +} + +// Shutdown gracefully shuts down the peer server. +func (s *Server) Shutdown() { + log.Debug("shutting down HTTP server") + s.grp.StopAndWait() + log.Debug("HTTP server stopped") +} + +// Start starts the server listener to handle connections. +func (s *Server) Start(address string) error { + gin.SetMode(gin.ReleaseMode) + router := gin.Default() + router.GET("/blob", s.getBlob) + router.HEAD("/blob", s.hasBlob) + srv := &http.Server{ + Addr: address, + Handler: router, + } + go s.listenForShutdown(srv) + // Initializing the server in a goroutine so that + // it won't block the graceful shutdown handling below + s.grp.Add(1) + go func() { + defer s.grp.Done() + log.Println("HTTP server listening on " + address) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + return nil +} + +func (s *Server) listenForShutdown(listener *http.Server) { + <-s.grp.Ch() + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := listener.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown:", err) + } +} diff --git a/shared/errors.go b/shared/errors.go new file mode 100644 index 0000000..f103394 --- /dev/null +++ b/shared/errors.go @@ -0,0 +1,6 @@ +package shared + +import "github.com/lbryio/lbry.go/v2/extras/errors" + +//ErrNotImplemented is a standard error when a store that implements the store interface does not implement a method +var ErrNotImplemented = errors.Base("this store does not implement this method") diff --git a/shared/shared.go b/shared/shared.go new file mode 100644 index 0000000..c232cf6 --- /dev/null +++ b/shared/shared.go @@ -0,0 +1,82 @@ +package shared + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "github.com/lbryio/lbry.go/v2/extras/errors" +) + +type BlobStack struct { + Timing time.Duration `json:"timing"` + OriginName string `json:"origin_name"` + HostName string `json:"host_name"` +} +type BlobTrace struct { + Stacks []BlobStack `json:"stacks"` +} + +var hostName *string + +func getHostName() string { + if hostName == nil { + hn, err := os.Hostname() + if err != nil { + hn = "unknown" + } + hostName = &hn + } + return *hostName +} +func (b *BlobTrace) Stack(timing time.Duration, originName string) BlobTrace { + b.Stacks = append(b.Stacks, BlobStack{ + Timing: timing, + OriginName: originName, + HostName: getHostName(), + }) + return *b +} +func (b *BlobTrace) Merge(otherTrance BlobTrace) BlobTrace { + b.Stacks = append(b.Stacks, otherTrance.Stacks...) + return *b +} +func NewBlobTrace(timing time.Duration, originName string) BlobTrace { + b := BlobTrace{} + b.Stacks = append(b.Stacks, BlobStack{ + Timing: timing, + OriginName: originName, + HostName: getHostName(), + }) + return b +} + +func (b BlobTrace) String() string { + var fullTrace string + for i, stack := range b.Stacks { + delta := time.Duration(0) + if i > 0 { + delta = stack.Timing - b.Stacks[i-1].Timing + } + fullTrace += fmt.Sprintf("[%d](%s) origin: %s - timing: %s - delta: %s\n", i, stack.HostName, stack.OriginName, stack.Timing.String(), delta.String()) + } + return fullTrace +} + +func (b BlobTrace) Serialize() (string, error) { + t, err := json.Marshal(b) + if err != nil { + return "", errors.Err(err) + } + return string(t), nil +} + +func Deserialize(serializedData string) (*BlobTrace, error) { + var trace BlobTrace + err := json.Unmarshal([]byte(serializedData), &trace) + if err != nil { + return nil, errors.Err(err) + } + return &trace, nil +} diff --git a/shared/shared_test.go b/shared/shared_test.go new file mode 100644 index 0000000..50a4eb7 --- /dev/null +++ b/shared/shared_test.go @@ -0,0 +1,44 @@ +package shared + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBlobTrace_Serialize(t *testing.T) { + hostName, err := os.Hostname() + require.NoError(t, err) + + stack := NewBlobTrace(10*time.Second, "test") + stack.Stack(20*time.Second, "test2") + stack.Stack(30*time.Second, "test3") + serialized, err := stack.Serialize() + require.NoError(t, err) + + expected := `{"stacks":[{"timing":10000000000,"origin_name":"test","host_name":"` + + hostName + + `"},{"timing":20000000000,"origin_name":"test2","host_name":"` + + hostName + + `"},{"timing":30000000000,"origin_name":"test3","host_name":"` + + hostName + + `"}]}` + assert.Equal(t, expected, serialized) +} + +func TestBlobTrace_Deserialize(t *testing.T) { + serialized := `{"stacks":[{"timing":10000000000,"origin_name":"test"},{"timing":20000000000,"origin_name":"test2"},{"timing":30000000000,"origin_name":"test3"}]}` + stack, err := Deserialize(serialized) + require.NoError(t, err) + + assert.Len(t, stack.Stacks, 3) + assert.Equal(t, stack.Stacks[0].Timing, 10*time.Second) + assert.Equal(t, stack.Stacks[1].Timing, 20*time.Second) + assert.Equal(t, stack.Stacks[2].Timing, 30*time.Second) + assert.Equal(t, stack.Stacks[0].OriginName, "test") + assert.Equal(t, stack.Stacks[1].OriginName, "test2") + assert.Equal(t, stack.Stacks[2].OriginName, "test3") +} diff --git a/store/caching.go b/store/caching.go index 0a06395..65b6397 100644 --- a/store/caching.go +++ b/store/caching.go @@ -5,6 +5,8 @@ import ( "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" + log "github.com/sirupsen/logrus" "github.com/lbryio/reflector.go/internal/metrics" ) @@ -22,7 +24,7 @@ func NewCachingStore(component string, origin, cache BlobStore) *CachingStore { return &CachingStore{ component: component, origin: WithSingleFlight(component, origin), - cache: cache, + cache: WithSingleFlight(component, cache), } } @@ -42,9 +44,9 @@ func (c *CachingStore) Has(hash string) (bool, error) { // Get tries to get the blob from the cache first, falling back to the origin. If the blob comes // from the origin, it is also stored in the cache. -func (c *CachingStore) Get(hash string) (stream.Blob, error) { +func (c *CachingStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { start := time.Now() - blob, err := c.cache.Get(hash) + blob, trace, err := c.cache.Get(hash) if err == nil || !errors.Is(err, ErrBlobNotFound) { metrics.CacheHitCount.With(metrics.CacheLabels(c.cache.Name(), c.component)).Inc() rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds() @@ -53,18 +55,26 @@ func (c *CachingStore) Get(hash string) (stream.Blob, error) { metrics.LabelComponent: c.component, metrics.LabelSource: "cache", }).Set(rate) - return blob, err + return blob, trace.Stack(time.Since(start), c.Name()), err } metrics.CacheMissCount.With(metrics.CacheLabels(c.cache.Name(), c.component)).Inc() - blob, err = c.origin.Get(hash) + blob, trace, err = c.origin.Get(hash) if err != nil { - return nil, err + return nil, trace.Stack(time.Since(start), c.Name()), err } - - err = c.cache.Put(hash, blob) - return blob, err + // there is no need to wait for the blob to be stored before we return it + // TODO: however this should be refactored to limit the amount of routines that the process can spawn to avoid a possible DoS + metrics.RoutinesQueue.WithLabelValues("store", "cache-put").Inc() + go func() { + defer metrics.RoutinesQueue.WithLabelValues("store", "cache-put").Dec() + err = c.cache.Put(hash, blob) + if err != nil { + log.Errorf("error saving blob to underlying cache: %s", errors.FullTrace(err)) + } + }() + return blob, trace.Stack(time.Since(start), c.Name()), nil } // Put stores the blob in the origin and the cache @@ -93,3 +103,10 @@ func (c *CachingStore) Delete(hash string) error { } return c.cache.Delete(hash) } + +// Shutdown shuts down the store gracefully +func (c *CachingStore) Shutdown() { + c.origin.Shutdown() + c.cache.Shutdown() + return +} diff --git a/store/caching_test.go b/store/caching_test.go index 34f928c..66a42fd 100644 --- a/store/caching_test.go +++ b/store/caching_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" ) func TestCachingStore_Put(t *testing.T) { @@ -51,13 +52,14 @@ func TestCachingStore_CacheMiss(t *testing.T) { t.Fatal(err) } - res, err := s.Get(hash) + res, stack, err := s.Get(hash) if err != nil { t.Fatal(err) } if !bytes.Equal(b, res) { t.Errorf("expected Get() to return %s, got %s", string(b), string(res)) } + time.Sleep(10 * time.Millisecond) //storing to cache is done async so let's give it some time has, err := cache.Has(hash) if err != nil { @@ -66,14 +68,16 @@ func TestCachingStore_CacheMiss(t *testing.T) { if !has { t.Errorf("Get() did not copy blob to cache") } + t.Logf("stack: %s", stack.String()) - res, err = cache.Get(hash) + res, stack, err = cache.Get(hash) if err != nil { t.Fatal(err) } if !bytes.Equal(b, res) { t.Errorf("expected cached Get() to return %s, got %s", string(b), string(res)) } + t.Logf("stack: %s", stack.String()) } func TestCachingStore_ThunderingHerd(t *testing.T) { @@ -92,7 +96,7 @@ func TestCachingStore_ThunderingHerd(t *testing.T) { wg := &sync.WaitGroup{} getNoErr := func() { - res, err := s.Get(hash) + res, _, err := s.Get(hash) if err != nil { t.Fatal(err) } @@ -148,7 +152,7 @@ func (s *SlowBlobStore) Has(hash string) (bool, error) { return s.mem.Has(hash) } -func (s *SlowBlobStore) Get(hash string) (stream.Blob, error) { +func (s *SlowBlobStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { time.Sleep(s.delay) return s.mem.Get(hash) } @@ -167,3 +171,7 @@ func (s *SlowBlobStore) Delete(hash string) error { time.Sleep(s.delay) return s.mem.Delete(hash) } + +func (s *SlowBlobStore) Shutdown() { + return +} diff --git a/store/cloudfront_ro.go b/store/cloudfront_ro.go index a914285..757174c 100644 --- a/store/cloudfront_ro.go +++ b/store/cloudfront_ro.go @@ -6,11 +6,11 @@ import ( "net/http" "time" - "github.com/lbryio/reflector.go/internal/metrics" - "github.com/lbryio/reflector.go/meta" - "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/meta" + "github.com/lbryio/reflector.go/shared" log "github.com/sirupsen/logrus" ) @@ -49,30 +49,30 @@ func (c *CloudFrontROStore) Has(hash string) (bool, error) { } // Get gets the blob from Cloudfront. -func (c *CloudFrontROStore) Get(hash string) (stream.Blob, error) { +func (c *CloudFrontROStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { log.Debugf("Getting %s from S3", hash[:8]) + start := time.Now() defer func(t time.Time) { log.Debugf("Getting %s from S3 took %s", hash[:8], time.Since(t).String()) - }(time.Now()) + }(start) status, body, err := c.cfRequest(http.MethodGet, hash) if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), c.Name()), err } defer body.Close() - switch status { case http.StatusNotFound, http.StatusForbidden: - return nil, errors.Err(ErrBlobNotFound) + return nil, shared.NewBlobTrace(time.Since(start), c.Name()), errors.Err(ErrBlobNotFound) case http.StatusOK: b, err := ioutil.ReadAll(body) if err != nil { - return nil, errors.Err(err) + return nil, shared.NewBlobTrace(time.Since(start), c.Name()), errors.Err(err) } metrics.MtrInBytesS3.Add(float64(len(b))) - return b, nil + return b, shared.NewBlobTrace(time.Since(start), c.Name()), nil default: - return nil, errors.Err("unexpected status %d", status) + return nil, shared.NewBlobTrace(time.Since(start), c.Name()), errors.Err("unexpected status %d", status) } } @@ -93,13 +93,18 @@ func (c *CloudFrontROStore) cfRequest(method, hash string) (int, io.ReadCloser, } func (c *CloudFrontROStore) Put(_ string, _ stream.Blob) error { - panic("CloudFrontROStore cannot do writes. Use CloudFrontRWStore") + return errors.Err(shared.ErrNotImplemented) } func (c *CloudFrontROStore) PutSD(_ string, _ stream.Blob) error { - panic("CloudFrontROStore cannot do writes. Use CloudFrontRWStore") + return errors.Err(shared.ErrNotImplemented) } func (c *CloudFrontROStore) Delete(_ string) error { - panic("CloudFrontROStore cannot do writes. Use CloudFrontRWStore") + return errors.Err(shared.ErrNotImplemented) +} + +// Shutdown shuts down the store gracefully +func (c *CloudFrontROStore) Shutdown() { + return } diff --git a/store/cloudfront_rw.go b/store/cloudfront_rw.go index ee771da..6b293a8 100644 --- a/store/cloudfront_rw.go +++ b/store/cloudfront_rw.go @@ -1,18 +1,21 @@ package store import ( + "time" + "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" ) -// CloudFrontRWStore combines a Cloudfront and an S3 store. Reads go to Cloudfront, writes go to S3. +// CloudFrontRWStore combines a Cloudfront and an S3 store. Reads go to Cloudfront/Wasabi, writes go to S3. type CloudFrontRWStore struct { - cf *CloudFrontROStore + cf *ITTTStore s3 *S3Store } // NewCloudFrontRWStore returns an initialized CloudFrontRWStore store pointer. // NOTE: It panics if either argument is nil. -func NewCloudFrontRWStore(cf *CloudFrontROStore, s3 *S3Store) *CloudFrontRWStore { +func NewCloudFrontRWStore(cf *ITTTStore, s3 *S3Store) *CloudFrontRWStore { if cf == nil || s3 == nil { panic("both stores must be set") } @@ -30,8 +33,10 @@ func (c *CloudFrontRWStore) Has(hash string) (bool, error) { } // Get gets the blob from Cloudfront. -func (c *CloudFrontRWStore) Get(hash string) (stream.Blob, error) { - return c.cf.Get(hash) +func (c *CloudFrontRWStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + blob, trace, err := c.cf.Get(hash) + return blob, trace.Stack(time.Since(start), c.Name()), err } // Put stores the blob on S3 @@ -48,3 +53,10 @@ func (c *CloudFrontRWStore) PutSD(hash string, blob stream.Blob) error { func (c *CloudFrontRWStore) Delete(hash string) error { return c.s3.Delete(hash) } + +// Shutdown shuts down the store gracefully +func (c *CloudFrontRWStore) Shutdown() { + c.s3.Shutdown() + c.cf.Shutdown() + return +} diff --git a/store/dbbacked.go b/store/dbbacked.go index 59c47b5..90c9fed 100644 --- a/store/dbbacked.go +++ b/store/dbbacked.go @@ -3,26 +3,28 @@ package store import ( "encoding/json" "sync" - - "github.com/lbryio/reflector.go/db" + "time" "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/db" + "github.com/lbryio/reflector.go/shared" log "github.com/sirupsen/logrus" ) // DBBackedStore is a store that's backed by a DB. The DB contains data about what's in the store. type DBBackedStore struct { - blobs BlobStore - db *db.SQL - blockedMu sync.RWMutex - blocked map[string]bool + blobs BlobStore + db *db.SQL + blockedMu sync.RWMutex + blocked map[string]bool + deleteOnMiss bool } // NewDBBackedStore returns an initialized store pointer. -func NewDBBackedStore(blobs BlobStore, db *db.SQL) *DBBackedStore { - return &DBBackedStore{blobs: blobs, db: db} +func NewDBBackedStore(blobs BlobStore, db *db.SQL, deleteOnMiss bool) *DBBackedStore { + return &DBBackedStore{blobs: blobs, db: db, deleteOnMiss: deleteOnMiss} } const nameDBBacked = "db-backed" @@ -32,20 +34,29 @@ func (d *DBBackedStore) Name() string { return nameDBBacked } // Has returns true if the blob is in the store func (d *DBBackedStore) Has(hash string) (bool, error) { - return d.db.HasBlob(hash) + return d.db.HasBlob(hash, false) } // Get gets the blob -func (d *DBBackedStore) Get(hash string) (stream.Blob, error) { - has, err := d.db.HasBlob(hash) +func (d *DBBackedStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + has, err := d.db.HasBlob(hash, true) if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), err } if !has { - return nil, ErrBlobNotFound + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), ErrBlobNotFound + } + + b, stack, err := d.blobs.Get(hash) + if d.deleteOnMiss && errors.Is(err, ErrBlobNotFound) { + e2 := d.Delete(hash) + if e2 != nil { + log.Errorf("error while deleting blob from db: %s", errors.FullTrace(err)) + } } - return d.blobs.Get(hash) + return b, stack.Stack(time.Since(start), d.Name()), err } // Put stores the blob in the S3 store and stores the blob information in the DB. @@ -100,7 +111,7 @@ func (d *DBBackedStore) Block(hash string) error { return err } - has, err := d.db.HasBlob(hash) + has, err := d.db.HasBlob(hash, false) if err != nil { return err } @@ -182,3 +193,9 @@ func (d *DBBackedStore) initBlocked() error { return err } + +// Shutdown shuts down the store gracefully +func (d *DBBackedStore) Shutdown() { + d.blobs.Shutdown() + return +} diff --git a/store/disk.go b/store/disk.go index 2234971..5ef7006 100644 --- a/store/disk.go +++ b/store/disk.go @@ -1,15 +1,42 @@ package store import ( + "crypto/sha512" + "encoding/hex" + "fmt" "io/ioutil" "os" "path" + "runtime" + "time" "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" "github.com/lbryio/reflector.go/store/speedwalk" + log "github.com/sirupsen/logrus" + "go.uber.org/atomic" ) +func init() { + writeCh = make(chan writeRequest) + for i := 0; i < runtime.NumCPU(); i++ { + go func() { + for { + select { + case r := <-writeCh: + err := ioutil.WriteFile(r.filename, r.data, r.perm) + if err != nil { + log.Errorf("could not write file %s to disk, failed with error: %s", r.filename, err.Error()) + } + } + } + }() + } +} + +var writeCh chan writeRequest + // DiskStore stores blobs on a local disk type DiskStore struct { // the location of blobs on disk @@ -19,8 +46,12 @@ type DiskStore struct { // true if initOnce ran, false otherwise initialized bool + + concurrentChecks atomic.Int32 } +const maxConcurrentChecks = 3 + // NewDiskStore returns an initialized file disk store pointer. func NewDiskStore(dir string, prefixLength int) *DiskStore { return &DiskStore{ @@ -52,21 +83,40 @@ func (d *DiskStore) Has(hash string) (bool, error) { } // Get returns the blob or an error if the blob doesn't exist. -func (d *DiskStore) Get(hash string) (stream.Blob, error) { +func (d *DiskStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() err := d.initOnce() if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), err } blob, err := ioutil.ReadFile(d.path(hash)) if err != nil { if os.IsNotExist(err) { - return nil, errors.Err(ErrBlobNotFound) + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), errors.Err(ErrBlobNotFound) + } + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), errors.Err(err) + } + + // this is a rather poor yet effective way of throttling how many blobs can be checked concurrently + // poor because there is a possible race condition between the check and the actual +1 + if d.concurrentChecks.Load() < maxConcurrentChecks { + d.concurrentChecks.Add(1) + defer d.concurrentChecks.Sub(1) + hashBytes := sha512.Sum384(blob) + readHash := hex.EncodeToString(hashBytes[:]) + if hash != readHash { + message := fmt.Sprintf("[%s] found a broken blob while reading from disk. Actual hash: %s", hash, readHash) + log.Errorf("%s", message) + err := d.Delete(hash) + if err != nil { + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), err + } + return nil, shared.NewBlobTrace(time.Since(start), d.Name()), errors.Err(message) } - return nil, errors.Err(err) } - return blob, nil + return blob, shared.NewBlobTrace(time.Since(start), d.Name()), nil } // Put stores the blob on disk @@ -80,8 +130,7 @@ func (d *DiskStore) Put(hash string, blob stream.Blob) error { if err != nil { return err } - - err = ioutil.WriteFile(d.path(hash), blob, 0644) + writeFile(d.path(hash), blob, 0644) return errors.Err(err) } @@ -147,3 +196,22 @@ func (d *DiskStore) initOnce() error { d.initialized = true return nil } + +type writeRequest struct { + filename string + data []byte + perm os.FileMode +} + +// Shutdown shuts down the store gracefully +func (d *DiskStore) Shutdown() { + return +} + +func writeFile(filename string, data []byte, perm os.FileMode) { + writeCh <- writeRequest{ + filename: filename, + data: data, + perm: perm, + } +} diff --git a/store/disk_test.go b/store/disk_test.go index 3bc088a..1ab3f05 100644 --- a/store/disk_test.go +++ b/store/disk_test.go @@ -19,7 +19,7 @@ func TestDiskStore_Get(t *testing.T) { defer os.RemoveAll(tmpDir) d := NewDiskStore(tmpDir, 2) - hash := "1234567890" + hash := "f428b8265d65dad7f8ffa52922bba836404cbd62f3ecfe10adba6b444f8f658938e54f5981ac4de39644d5b93d89a94b" data := []byte("oyuntyausntoyaunpdoyruoyduanrstjwfjyuwf") expectedPath := path.Join(tmpDir, hash[:2], hash) @@ -28,7 +28,7 @@ func TestDiskStore_Get(t *testing.T) { err = ioutil.WriteFile(expectedPath, data, os.ModePerm) require.NoError(t, err) - blob, err := d.Get(hash) + blob, _, err := d.Get(hash) assert.NoError(t, err) assert.EqualValues(t, data, blob) } @@ -39,7 +39,7 @@ func TestDiskStore_GetNonexistentBlob(t *testing.T) { defer os.RemoveAll(tmpDir) d := NewDiskStore(tmpDir, 2) - blob, err := d.Get("nonexistent") + blob, _, err := d.Get("nonexistent") assert.Nil(t, blob) assert.True(t, errors.Is(err, ErrBlobNotFound)) } diff --git a/store/http.go b/store/http.go new file mode 100644 index 0000000..c24169a --- /dev/null +++ b/store/http.go @@ -0,0 +1,160 @@ +package store + +import ( + "bytes" + "io" + "io/ioutil" + "net" + "net/http" + "sync" + "time" + + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" + + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/lbry.go/v2/stream" +) + +// NoopStore is a store that does nothing +type HttpStore struct { + upstream string + httpClient *http.Client +} + +func NewHttpStore(upstream string) *HttpStore { + return &HttpStore{ + upstream: "http://" + upstream, + httpClient: getClient(), + } +} + +const nameHttp = "http" + +func (n *HttpStore) Name() string { return nameNoop } +func (n *HttpStore) Has(hash string) (bool, error) { + url := n.upstream + "/blob?hash=" + hash + + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return false, errors.Err(err) + } + + res, err := n.httpClient.Do(req) + if err != nil { + return false, errors.Err(err) + } + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { + return false, nil + } + if res.StatusCode == http.StatusNoContent { + return true, nil + } + var body []byte + if res.Body != nil { + body, _ = ioutil.ReadAll(res.Body) + } + return false, errors.Err("upstream error. Status code: %d (%s)", res.StatusCode, string(body)) +} + +func (n *HttpStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + url := n.upstream + "/blob?hash=" + hash + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, shared.NewBlobTrace(time.Since(start), n.Name()), errors.Err(err) + } + + res, err := n.httpClient.Do(req) + if err != nil { + return nil, shared.NewBlobTrace(time.Since(start), n.Name()), errors.Err(err) + } + defer res.Body.Close() + tmp := getBuffer() + defer putBuffer(tmp) + serialized := res.Header.Get("Via") + trace := shared.NewBlobTrace(time.Since(start), n.Name()) + if serialized != "" { + parsedTrace, err := shared.Deserialize(serialized) + if err != nil { + return nil, shared.NewBlobTrace(time.Since(start), n.Name()), err + } + trace = *parsedTrace + } + + if res.StatusCode == http.StatusNotFound { + return nil, trace.Stack(time.Since(start), n.Name()), ErrBlobNotFound + } + if res.StatusCode == http.StatusOK { + written, err := io.Copy(tmp, res.Body) + if err != nil { + return nil, trace.Stack(time.Since(start), n.Name()), errors.Err(err) + } + + blob := make([]byte, written) + copy(blob, tmp.Bytes()) + metrics.MtrInBytesHttp.Add(float64(len(blob))) + return blob, trace.Stack(time.Since(start), n.Name()), nil + } + var body []byte + if res.Body != nil { + body, _ = ioutil.ReadAll(res.Body) + } + + return nil, trace.Stack(time.Since(start), n.Name()), errors.Err("upstream error. Status code: %d (%s)", res.StatusCode, string(body)) +} + +func (n *HttpStore) Put(string, stream.Blob) error { + return shared.ErrNotImplemented +} +func (n *HttpStore) PutSD(string, stream.Blob) error { + return shared.ErrNotImplemented +} +func (n *HttpStore) Delete(string) error { + return shared.ErrNotImplemented +} +func (n *HttpStore) Shutdown() { return } + +// buffer pool to reduce GC +// https://www.captaincodeman.com/2017/06/02/golang-buffer-pool-gotcha +var buffers = sync.Pool{ + // New is called when a new instance is needed + New: func() interface{} { + buf := make([]byte, 0, stream.MaxBlobSize) + return bytes.NewBuffer(buf) + }, +} + +// getBuffer fetches a buffer from the pool +func getBuffer() *bytes.Buffer { + return buffers.Get().(*bytes.Buffer) +} + +// putBuffer returns a buffer to the pool +func putBuffer(buf *bytes.Buffer) { + buf.Reset() + buffers.Put(buf) +} + +// getClient gets an http client that's customized to be more performant when dealing with blobs of 2MB in size (most of our blobs) +func getClient() *http.Client { + // Customize the Transport to have larger connection pool + defaultTransport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DisableCompression: true, + MaxIdleConnsPerHost: 100, + ReadBufferSize: stream.MaxBlobSize + 1024*10, //add an extra few KBs to make sure it fits the extra information + } + + return &http.Client{Transport: defaultTransport} +} diff --git a/store/ittt.go b/store/ittt.go new file mode 100644 index 0000000..5edc67d --- /dev/null +++ b/store/ittt.go @@ -0,0 +1,75 @@ +package store + +import ( + "time" + + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/lbry.go/v2/stream" + + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" +) + +// ITTT store performs an operation on this storage, if this fails, it attempts to run it on that +type ITTTStore struct { + this, that BlobStore +} + +// NewCachingStore makes a new caching disk store and returns a pointer to it. +func NewITTTStore(this, that BlobStore) *ITTTStore { + return &ITTTStore{ + this: this, + that: that, + } +} + +const nameIttt = "ittt" + +// Name is the cache type name +func (c *ITTTStore) Name() string { return nameIttt } + +// Has checks the cache and then the origin for a hash. It returns true if either store has it. +func (c *ITTTStore) Has(hash string) (bool, error) { + has, err := c.this.Has(hash) + if err != nil || !has { + has, err = c.that.Has(hash) + } + return has, err +} + +// Get tries to get the blob from this first, falling back to that. +func (c *ITTTStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + blob, trace, err := c.this.Get(hash) + if err == nil { + metrics.ThisHitCount.Inc() + return blob, trace.Stack(time.Since(start), c.Name()), err + } + + blob, trace, err = c.that.Get(hash) + if err != nil { + return nil, trace.Stack(time.Since(start), c.Name()), err + } + metrics.ThatHitCount.Inc() + return blob, trace.Stack(time.Since(start), c.Name()), nil +} + +// Put not implemented +func (c *ITTTStore) Put(hash string, blob stream.Blob) error { + return errors.Err(shared.ErrNotImplemented) +} + +// PutSD not implemented +func (c *ITTTStore) PutSD(hash string, blob stream.Blob) error { + return errors.Err(shared.ErrNotImplemented) +} + +// Delete not implemented +func (c *ITTTStore) Delete(hash string) error { + return errors.Err(shared.ErrNotImplemented) +} + +// Shutdown shuts down the store gracefully +func (c *ITTTStore) Shutdown() { + return +} diff --git a/store/lfuda.go b/store/lfuda.go new file mode 100644 index 0000000..b0437b0 --- /dev/null +++ b/store/lfuda.go @@ -0,0 +1,132 @@ +package store + +import ( + "time" + + "github.com/bparli/lfuda-go" + "github.com/lbryio/lbry.go/v2/extras/errors" + "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" + "github.com/sirupsen/logrus" +) + +// LRUStore adds a max cache size and LRU eviction to a BlobStore +type LFUDAStore struct { + // underlying store + store BlobStore + // lfuda implementation + lfuda *lfuda.Cache +} + +// NewLRUStore initialize a new LRUStore +func NewLFUDAStore(component string, store BlobStore, maxSize float64) *LFUDAStore { + lfuda := lfuda.NewGDSFWithEvict(maxSize, func(key interface{}, value interface{}) { + metrics.CacheLRUEvictCount.With(metrics.CacheLabels(store.Name(), component)).Inc() + _ = store.Delete(key.(string)) // TODO: log this error. may happen if underlying entry is gone but cache entry still there + }) + l := &LFUDAStore{ + store: store, + lfuda: lfuda, + } + go func() { + if lstr, ok := store.(lister); ok { + err := l.loadExisting(lstr, int(maxSize)) + if err != nil { + panic(err) // TODO: what should happen here? panic? return nil? just keep going? + } + } + }() + + return l +} + +const nameLFUDA = "lfuda" + +// Name is the cache type name +func (l *LFUDAStore) Name() string { return nameLFUDA } + +// Has returns whether the blob is in the store, without updating the recent-ness. +func (l *LFUDAStore) Has(hash string) (bool, error) { + return l.lfuda.Contains(hash), nil +} + +// Get returns the blob or an error if the blob doesn't exist. +func (l *LFUDAStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + _, has := l.lfuda.Get(hash) + if !has { + return nil, shared.NewBlobTrace(time.Since(start), l.Name()), errors.Err(ErrBlobNotFound) + } + blob, stack, err := l.store.Get(hash) + if errors.Is(err, ErrBlobNotFound) { + // Blob disappeared from underlying store + l.lfuda.Remove(hash) + } + return blob, stack.Stack(time.Since(start), l.Name()), err +} + +// Put stores the blob. Following LFUDA rules it's not guaranteed that a SET will store the value!!! +func (l *LFUDAStore) Put(hash string, blob stream.Blob) error { + l.lfuda.Set(hash, true) + has, _ := l.Has(hash) + if has { + err := l.store.Put(hash, blob) + if err != nil { + return err + } + } + return nil +} + +// PutSD stores the sd blob. Following LFUDA rules it's not guaranteed that a SET will store the value!!! +func (l *LFUDAStore) PutSD(hash string, blob stream.Blob) error { + l.lfuda.Set(hash, true) + has, _ := l.Has(hash) + if has { + err := l.store.PutSD(hash, blob) + if err != nil { + return err + } + } + return nil +} + +// Delete deletes the blob from the store +func (l *LFUDAStore) Delete(hash string) error { + err := l.store.Delete(hash) + if err != nil { + return err + } + + // This must come after store.Delete() + // Remove triggers onEvict function, which also tries to delete blob from store + // We need to delete it manually first so any errors can be propagated up + l.lfuda.Remove(hash) + return nil +} + +// loadExisting imports existing blobs from the underlying store into the LRU cache +func (l *LFUDAStore) loadExisting(store lister, maxItems int) error { + logrus.Infof("loading at most %d items", maxItems) + existing, err := store.list() + if err != nil { + return err + } + logrus.Infof("read %d files from underlying store", len(existing)) + + added := 0 + for _, h := range existing { + l.lfuda.Set(h, true) + added++ + if maxItems > 0 && added >= maxItems { // underlying cache is bigger than the cache + break + } + } + return nil +} + +// Shutdown shuts down the store gracefully +func (l *LFUDAStore) Shutdown() { + return +} diff --git a/store/lfuda_test.go b/store/lfuda_test.go new file mode 100644 index 0000000..d8657ac --- /dev/null +++ b/store/lfuda_test.go @@ -0,0 +1,136 @@ +package store + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + "time" + + "github.com/lbryio/lbry.go/v2/extras/errors" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const cacheMaxSize = 3 + +func getTestLFUDAStore() (*LFUDAStore, *MemStore) { + m := NewMemStore() + return NewLFUDAStore("test", m, cacheMaxSize), m +} + +func TestFUDAStore_Eviction(t *testing.T) { + lfuda, mem := getTestLFUDAStore() + b := []byte("x") + err := lfuda.Put("one", b) + require.NoError(t, err) + err = lfuda.Put("two", b) + require.NoError(t, err) + err = lfuda.Put("three", b) + require.NoError(t, err) + err = lfuda.Put("four", b) + require.NoError(t, err) + err = lfuda.Put("five", b) + require.NoError(t, err) + err = lfuda.Put("five", b) + require.NoError(t, err) + err = lfuda.Put("four", b) + require.NoError(t, err) + err = lfuda.Put("two", b) + require.NoError(t, err) + + _, _, err = lfuda.Get("five") + require.NoError(t, err) + _, _, err = lfuda.Get("four") + require.NoError(t, err) + _, _, err = lfuda.Get("two") + require.NoError(t, err) + assert.Equal(t, cacheMaxBlobs, len(mem.Debug())) + + for k, v := range map[string]bool{ + "one": false, + "two": true, + "three": false, + "four": true, + "five": true, + "six": false, + } { + has, err := lfuda.Has(k) + assert.NoError(t, err) + assert.Equal(t, v, has) + } + + lfuda.Get("two") // touch so it stays in cache + lfuda.Get("five") // touch so it stays in cache + lfuda.Put("six", b) + + assert.Equal(t, cacheMaxBlobs, len(mem.Debug())) + + for k, v := range map[string]bool{ + "one": false, + "two": true, + "three": false, + "four": false, + "five": true, + "six": true, + } { + has, err := lfuda.Has(k) + assert.NoError(t, err) + assert.Equal(t, v, has) + } + + err = lfuda.Delete("six") + assert.NoError(t, err) + err = lfuda.Delete("five") + assert.NoError(t, err) + err = lfuda.Delete("two") + assert.NoError(t, err) + assert.Equal(t, 0, len(mem.Debug())) +} + +func TestFUDAStore_UnderlyingBlobMissing(t *testing.T) { + lfuda, mem := getTestLFUDAStore() + hash := "hash" + b := []byte("this is a blob of stuff") + err := lfuda.Put(hash, b) + require.NoError(t, err) + + err = mem.Delete(hash) + require.NoError(t, err) + + // hash still exists in lru + assert.True(t, lfuda.lfuda.Contains(hash)) + + blob, _, err := lfuda.Get(hash) + assert.Nil(t, blob) + assert.True(t, errors.Is(err, ErrBlobNotFound), "expected (%s) %s, got (%s) %s", + reflect.TypeOf(ErrBlobNotFound).String(), ErrBlobNotFound.Error(), + reflect.TypeOf(err).String(), err.Error()) + + // lru.Get() removes hash if underlying store doesn't have it + assert.False(t, lfuda.lfuda.Contains(hash)) +} + +func TestFUDAStore_loadExisting(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "reflector_test_*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + d := NewDiskStore(tmpDir, 2) + + hash := "hash" + b := []byte("this is a blob of stuff") + err = d.Put(hash, b) + require.NoError(t, err) + + existing, err := d.list() + require.NoError(t, err) + require.Equal(t, 1, len(existing), "blob should exist in cache") + assert.Equal(t, hash, existing[0]) + + lfuda := NewLFUDAStore("test", d, 3) // lru should load existing blobs when it's created + time.Sleep(100 * time.Millisecond) // async load so let's wait... + has, err := lfuda.Has(hash) + require.NoError(t, err) + assert.True(t, has, "hash should be loaded from disk store but it's not") +} diff --git a/store/lru.go b/store/lru.go index 4ef4908..be6cd82 100644 --- a/store/lru.go +++ b/store/lru.go @@ -1,11 +1,16 @@ package store import ( + "time" + + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" + "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" - "github.com/lbryio/reflector.go/internal/metrics" - golru "github.com/hashicorp/golang-lru" + "github.com/bluele/gcache" + "github.com/sirupsen/logrus" ) // LRUStore adds a max cache size and LRU eviction to a BlobStore @@ -13,56 +18,54 @@ type LRUStore struct { // underlying store store BlobStore // lru implementation - lru *golru.Cache + lru gcache.Cache } // NewLRUStore initialize a new LRUStore func NewLRUStore(component string, store BlobStore, maxItems int) *LRUStore { - lru, err := golru.NewWithEvict(maxItems, func(key interface{}, value interface{}) { - metrics.CacheLRUEvictCount.With(metrics.CacheLabels(store.Name(), component)).Inc() - _ = store.Delete(key.(string)) // TODO: log this error. may happen if underlying entry is gone but cache entry still there - }) - if err != nil { - panic(err) - } - l := &LRUStore{ store: store, - lru: lru, } - - if lstr, ok := store.(lister); ok { - err = l.loadExisting(lstr, maxItems) - if err != nil { - panic(err) // TODO: what should happen here? panic? return nil? just keep going? + l.lru = gcache.New(maxItems).ARC().EvictedFunc(func(key, value interface{}) { + metrics.CacheLRUEvictCount.With(metrics.CacheLabels(l.Name(), component)).Inc() + _ = store.Delete(key.(string)) + }).Build() + + go func() { + if lstr, ok := store.(lister); ok { + err := l.loadExisting(lstr, maxItems) + if err != nil { + panic(err) // TODO: what should happen here? panic? return nil? just keep going? + } } - } + }() return l } -const nameLRU = "lru" - // Name is the cache type name -func (l *LRUStore) Name() string { return nameLRU } +func (l *LRUStore) Name() string { + return "lru_" + l.store.Name() +} // Has returns whether the blob is in the store, without updating the recent-ness. func (l *LRUStore) Has(hash string) (bool, error) { - return l.lru.Contains(hash), nil + return l.lru.Has(hash), nil } // Get returns the blob or an error if the blob doesn't exist. -func (l *LRUStore) Get(hash string) (stream.Blob, error) { - _, has := l.lru.Get(hash) - if !has { - return nil, errors.Err(ErrBlobNotFound) +func (l *LRUStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + _, err := l.lru.Get(hash) + if err != nil { + return nil, shared.NewBlobTrace(time.Since(start), l.Name()), errors.Err(ErrBlobNotFound) } - blob, err := l.store.Get(hash) + blob, stack, err := l.store.Get(hash) if errors.Is(err, ErrBlobNotFound) { // Blob disappeared from underlying store l.lru.Remove(hash) } - return blob, err + return blob, stack.Stack(time.Since(start), l.Name()), err } // Put stores the blob @@ -72,7 +75,7 @@ func (l *LRUStore) Put(hash string, blob stream.Blob) error { return err } - l.lru.Add(hash, true) + l.lru.Set(hash, true) return nil } @@ -83,7 +86,7 @@ func (l *LRUStore) PutSD(hash string, blob stream.Blob) error { return err } - l.lru.Add(hash, true) + _ = l.lru.Set(hash, true) return nil } @@ -103,14 +106,15 @@ func (l *LRUStore) Delete(hash string) error { // loadExisting imports existing blobs from the underlying store into the LRU cache func (l *LRUStore) loadExisting(store lister, maxItems int) error { + logrus.Infof("loading at most %d items", maxItems) existing, err := store.list() if err != nil { return err } - + logrus.Infof("read %d files from disk", len(existing)) added := 0 for _, h := range existing { - l.lru.Add(h, true) + l.lru.Set(h, true) added++ if maxItems > 0 && added >= maxItems { // underlying cache is bigger than LRU cache break @@ -118,3 +122,8 @@ func (l *LRUStore) loadExisting(store lister, maxItems int) error { } return nil } + +// Shutdown shuts down the store gracefully +func (l *LRUStore) Shutdown() { + return +} diff --git a/store/lru_test.go b/store/lru_test.go index 968956c..798d5c2 100644 --- a/store/lru_test.go +++ b/store/lru_test.go @@ -5,6 +5,7 @@ import ( "os" "reflect" "testing" + "time" "github.com/lbryio/lbry.go/v2/extras/errors" @@ -86,16 +87,16 @@ func TestLRUStore_UnderlyingBlobMissing(t *testing.T) { require.NoError(t, err) // hash still exists in lru - assert.True(t, lru.lru.Contains(hash)) + assert.True(t, lru.lru.Has(hash)) - blob, err := lru.Get(hash) + blob, _, err := lru.Get(hash) assert.Nil(t, blob) assert.True(t, errors.Is(err, ErrBlobNotFound), "expected (%s) %s, got (%s) %s", reflect.TypeOf(ErrBlobNotFound).String(), ErrBlobNotFound.Error(), reflect.TypeOf(err).String(), err.Error()) // lru.Get() removes hash if underlying store doesn't have it - assert.False(t, lru.lru.Contains(hash)) + assert.False(t, lru.lru.Has(hash)) } func TestLRUStore_loadExisting(t *testing.T) { @@ -114,7 +115,8 @@ func TestLRUStore_loadExisting(t *testing.T) { require.Equal(t, 1, len(existing), "blob should exist in cache") assert.Equal(t, hash, existing[0]) - lru := NewLRUStore("test", d, 3) // lru should load existing blobs when it's created + lru := NewLRUStore("test", d, 3) // lru should load existing blobs when it's created + time.Sleep(100 * time.Millisecond) // async load so let's wait... has, err := lru.Has(hash) require.NoError(t, err) assert.True(t, has, "hash should be loaded from disk store but it's not") diff --git a/store/memory.go b/store/memory.go index f462a8d..30d8dea 100644 --- a/store/memory.go +++ b/store/memory.go @@ -2,9 +2,11 @@ package store import ( "sync" + "time" "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" ) // MemStore is an in memory only blob store with no persistence. @@ -34,14 +36,15 @@ func (m *MemStore) Has(hash string) (bool, error) { } // Get returns the blob byte slice if present and errors if the blob is not found. -func (m *MemStore) Get(hash string) (stream.Blob, error) { +func (m *MemStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() m.mu.RLock() defer m.mu.RUnlock() blob, ok := m.blobs[hash] if !ok { - return nil, errors.Err(ErrBlobNotFound) + return nil, shared.NewBlobTrace(time.Since(start), m.Name()), errors.Err(ErrBlobNotFound) } - return blob, nil + return blob, shared.NewBlobTrace(time.Since(start), m.Name()), nil } // Put stores the blob in memory @@ -71,3 +74,8 @@ func (m *MemStore) Debug() map[string]stream.Blob { defer m.mu.RUnlock() return m.blobs } + +// Shutdown shuts down the store gracefully +func (m *MemStore) Shutdown() { + return +} diff --git a/store/memory_test.go b/store/memory_test.go index 8d85114..775850b 100644 --- a/store/memory_test.go +++ b/store/memory_test.go @@ -25,7 +25,7 @@ func TestMemStore_Get(t *testing.T) { t.Error("error getting memory blob - ", err) } - gotBlob, err := s.Get(hash) + gotBlob, _, err := s.Get(hash) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -33,7 +33,7 @@ func TestMemStore_Get(t *testing.T) { t.Error("Got blob that is different from expected blob") } - missingBlob, err := s.Get("nonexistent hash") + missingBlob, _, err := s.Get("nonexistent hash") if err == nil { t.Errorf("Expected ErrBlobNotFound, got nil") } diff --git a/store/noop.go b/store/noop.go index 9e5d815..9cadf5c 100644 --- a/store/noop.go +++ b/store/noop.go @@ -1,15 +1,23 @@ package store -import "github.com/lbryio/lbry.go/v2/stream" +import ( + "time" + + "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" +) // NoopStore is a store that does nothing type NoopStore struct{} const nameNoop = "noop" -func (n *NoopStore) Name() string { return nameNoop } -func (n *NoopStore) Has(_ string) (bool, error) { return false, nil } -func (n *NoopStore) Get(_ string) (stream.Blob, error) { return nil, nil } +func (n *NoopStore) Name() string { return nameNoop } +func (n *NoopStore) Has(_ string) (bool, error) { return false, nil } +func (n *NoopStore) Get(_ string) (stream.Blob, shared.BlobTrace, error) { + return nil, shared.NewBlobTrace(time.Since(time.Now()), n.Name()), nil +} func (n *NoopStore) Put(_ string, _ stream.Blob) error { return nil } func (n *NoopStore) PutSD(_ string, _ stream.Blob) error { return nil } func (n *NoopStore) Delete(_ string) error { return nil } +func (n *NoopStore) Shutdown() { return } diff --git a/store/s3.go b/store/s3.go index 53451be..75ba459 100644 --- a/store/s3.go +++ b/store/s3.go @@ -8,6 +8,7 @@ import ( "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" @@ -65,17 +66,18 @@ func (s *S3Store) Has(hash string) (bool, error) { } // Get returns the blob slice if present or errors on S3. -func (s *S3Store) Get(hash string) (stream.Blob, error) { +func (s *S3Store) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() //Todo-Need to handle error for blob doesn't exist for consistency. err := s.initOnce() if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), s.Name()), err } log.Debugf("Getting %s from S3", hash[:8]) defer func(t time.Time) { log.Debugf("Getting %s from S3 took %s", hash[:8], time.Since(t).String()) - }(time.Now()) + }(start) buf := &aws.WriteAtBuffer{} _, err = s3manager.NewDownloader(s.session).Download(buf, &s3.GetObjectInput{ @@ -86,15 +88,15 @@ func (s *S3Store) Get(hash string) (stream.Blob, error) { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case s3.ErrCodeNoSuchBucket: - return nil, errors.Err("bucket %s does not exist", s.bucket) + return nil, shared.NewBlobTrace(time.Since(start), s.Name()), errors.Err("bucket %s does not exist", s.bucket) case s3.ErrCodeNoSuchKey: - return nil, errors.Err(ErrBlobNotFound) + return nil, shared.NewBlobTrace(time.Since(start), s.Name()), errors.Err(ErrBlobNotFound) } } - return buf.Bytes(), err + return buf.Bytes(), shared.NewBlobTrace(time.Since(start), s.Name()), err } - return buf.Bytes(), nil + return buf.Bytes(), shared.NewBlobTrace(time.Since(start), s.Name()), nil } // Put stores the blob on S3 or errors if S3 connection errors. @@ -110,10 +112,11 @@ func (s *S3Store) Put(hash string, blob stream.Blob) error { }(time.Now()) _, err = s3manager.NewUploader(s.session).Upload(&s3manager.UploadInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(hash), - Body: bytes.NewBuffer(blob), - StorageClass: aws.String(s3.StorageClassIntelligentTiering), + Bucket: aws.String(s.bucket), + Key: aws.String(hash), + Body: bytes.NewBuffer(blob), + ACL: aws.String("public-read"), + //StorageClass: aws.String(s3.StorageClassIntelligentTiering), }) metrics.MtrOutBytesReflector.Add(float64(blob.Size())) @@ -150,6 +153,7 @@ func (s *S3Store) initOnce() error { sess, err := session.NewSession(&aws.Config{ Credentials: credentials.NewStaticCredentials(s.awsID, s.awsSecret, ""), Region: aws.String(s.region), + Endpoint: aws.String("https://s3.wasabisys.com"), }) if err != nil { return err @@ -158,3 +162,8 @@ func (s *S3Store) initOnce() error { s.session = sess return nil } + +// Shutdown shuts down the store gracefully +func (s *S3Store) Shutdown() { + return +} diff --git a/store/singleflight.go b/store/singleflight.go index fbe314f..02337a2 100644 --- a/store/singleflight.go +++ b/store/singleflight.go @@ -3,7 +3,9 @@ package store import ( "time" + "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/reflector.go/shared" "github.com/lbryio/lbry.go/v2/stream" @@ -29,39 +31,98 @@ func (s *singleflightStore) Name() string { return "sf_" + s.BlobStore.Name() } +type getterResponse struct { + blob stream.Blob + stack shared.BlobTrace +} + // Get ensures that only one request per hash is sent to the origin at a time, // thereby protecting against https://en.wikipedia.org/wiki/Thundering_herd_problem -func (s *singleflightStore) Get(hash string) (stream.Blob, error) { - metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Inc() - defer metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Dec() +func (s *singleflightStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) { + start := time.Now() + metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Inc() + defer metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Dec() - blob, err, _ := s.sf.Do(hash, s.getter(hash)) + gr, err, _ := s.sf.Do(hash, s.getter(hash)) if err != nil { - return nil, err + return nil, shared.NewBlobTrace(time.Since(start), s.Name()), err } - return blob.(stream.Blob), nil + if gr == nil { + return nil, shared.NewBlobTrace(time.Since(start), s.Name()), errors.Err("getter response is nil") + } + rsp := gr.(getterResponse) + return rsp.blob, rsp.stack, nil } // getter returns a function that gets a blob from the origin // only one getter per hash will be executing at a time func (s *singleflightStore) getter(hash string) func() (interface{}, error) { return func() (interface{}, error) { - metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Inc() - defer metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Dec() + metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Inc() + defer metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Dec() + + start := time.Now() + blob, stack, err := s.BlobStore.Get(hash) + if err != nil { + return getterResponse{ + blob: nil, + stack: stack.Stack(time.Since(start), s.Name()), + }, err + } + + rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds() + metrics.CacheRetrievalSpeed.With(map[string]string{ + metrics.LabelCacheType: s.Name(), + metrics.LabelComponent: s.component, + metrics.LabelSource: "origin", + }).Set(rate) + + return getterResponse{ + blob: blob, + stack: stack.Stack(time.Since(start), s.Name()), + }, nil + } +} + +// Put ensures that only one request per hash is sent to the origin at a time, +// thereby protecting against https://en.wikipedia.org/wiki/Thundering_herd_problem +func (s *singleflightStore) Put(hash string, blob stream.Blob) error { + metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Inc() + defer metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Dec() + + _, err, _ := s.sf.Do(hash, s.putter(hash, blob)) + if err != nil { + return err + } + return nil +} + +// putter returns a function that puts a blob from the origin +// only one putter per hash will be executing at a time +func (s *singleflightStore) putter(hash string, blob stream.Blob) func() (interface{}, error) { + return func() (interface{}, error) { + metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Inc() + defer metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Dec() start := time.Now() - blob, err := s.BlobStore.Get(hash) + err := s.BlobStore.Put(hash, blob) if err != nil { return nil, err } rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds() metrics.CacheRetrievalSpeed.With(map[string]string{ - metrics.LabelCacheType: s.BlobStore.Name(), + metrics.LabelCacheType: s.Name(), metrics.LabelComponent: s.component, metrics.LabelSource: "origin", }).Set(rate) - return blob, nil + return nil, nil } } + +// Shutdown shuts down the store gracefully +func (s *singleflightStore) Shutdown() { + s.BlobStore.Shutdown() + return +} diff --git a/store/speedwalk/speedwalk.go b/store/speedwalk/speedwalk.go index e2563ba..1d4b981 100644 --- a/store/speedwalk/speedwalk.go +++ b/store/speedwalk/speedwalk.go @@ -6,6 +6,8 @@ import ( "runtime" "sync" + "github.com/lbryio/reflector.go/internal/metrics" + "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/karrick/godirwalk" @@ -24,6 +26,7 @@ func AllFiles(startDir string, basename bool) ([]string, error) { paths := make([]string, 0, 1000) pathWG := &sync.WaitGroup{} pathWG.Add(1) + metrics.RoutinesQueue.WithLabelValues("speedwalk", "worker").Inc() go func() { defer pathWG.Done() for { @@ -60,7 +63,6 @@ func AllFiles(startDir string, basename bool) ([]string, error) { walkerWG.Done() goroutineLimiter <- struct{}{} }() - err = godirwalk.Walk(filepath.Join(startDir, dir), &godirwalk.Options{ Unsorted: true, // faster this way Callback: func(osPathname string, de *godirwalk.Dirent) error { @@ -84,6 +86,5 @@ func AllFiles(startDir string, basename bool) ([]string, error) { close(pathChan) pathWG.Wait() - return paths, nil } diff --git a/store/store.go b/store/store.go index 200ff61..bd9223b 100644 --- a/store/store.go +++ b/store/store.go @@ -3,6 +3,7 @@ package store import ( "github.com/lbryio/lbry.go/v2/extras/errors" "github.com/lbryio/lbry.go/v2/stream" + "github.com/lbryio/reflector.go/shared" ) // BlobStore is an interface for handling blob storage. @@ -12,13 +13,15 @@ type BlobStore interface { // Does blob exist in the store. Has(hash string) (bool, error) // Get the blob from the store. Must return ErrBlobNotFound if blob is not in store. - Get(hash string) (stream.Blob, error) + Get(hash string) (stream.Blob, shared.BlobTrace, error) // Put the blob into the store. Put(hash string, blob stream.Blob) error // Put an SD blob into the store. PutSD(hash string, blob stream.Blob) error // Delete the blob from the store. Delete(hash string) error + // Shutdown the store gracefully + Shutdown() } // Blocklister is a store that supports blocking blobs to prevent their inclusion in the store. diff --git a/wallet/client.go b/wallet/client.go index a4cb165..3dc0e8c 100644 --- a/wallet/client.go +++ b/wallet/client.go @@ -6,7 +6,7 @@ import ( "github.com/lbryio/chainquery/lbrycrd" "github.com/lbryio/lbry.go/v2/extras/errors" - "github.com/lbryio/lbry.go/v2/schema/claim" + "github.com/lbryio/lbry.go/v2/schema/stake" types "github.com/lbryio/types/v2/go" "github.com/btcsuite/btcutil" @@ -140,7 +140,7 @@ func (n *Node) GetClaimInTx(txid string, nout int) (*types.Claim, error) { return nil, errors.Err(err) } - ch, err := claim.DecodeClaimBytes(value, "") + ch, err := stake.DecodeClaimBytes(value, "") if err != nil { return nil, errors.Err(err) }