Compare commits
101 commits
insert_in_
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
4d81a43a8f | ||
|
b3f0d63b4d | ||
|
c880f0b80f | ||
|
085490e92b | ||
|
456fe53e01 | ||
|
778fc17adf | ||
|
e93c097fd9 | ||
|
0dfda70c70 | ||
|
6c082993cf | ||
|
08ed3c9f13 | ||
|
7f75602841 | ||
|
4d168ddefc | ||
|
b7abb77ea1 | ||
|
8c2a46752c | ||
|
0c8da4abe5 | ||
|
87680d806c | ||
|
e1c59b9b63 | ||
|
c79634706b | ||
|
f5d30b1a6e | ||
|
0177dd4ce0 | ||
|
5693529216 | ||
|
a1c2e92ca3 | ||
|
9c0554ef05 | ||
|
4e80f91a57 | ||
|
c211f83ba7 | ||
|
29d1ccf68c | ||
|
2f7d67794f | ||
|
4d8e7739d7 | ||
|
6fc0ceea2a | ||
|
ae0c7dd2bb | ||
|
4af5c2f4c6 | ||
|
def0a97f49 | ||
|
6dde793745 | ||
|
654cc44935 | ||
|
90d6d29452 | ||
|
b2272fef3a | ||
|
86f3e62aa8 | ||
|
e1b4f21e00 | ||
|
b4913ecedf | ||
|
63a574ec2f | ||
|
b8af3408e0 | ||
|
847089d0d6 | ||
|
170dfef3a8 | ||
|
2b458a6bd0 | ||
|
febfc51cb0 | ||
|
72be487262 | ||
|
94e7d81bd3 | ||
|
c6c779da39 | ||
|
2e101083e6 | ||
|
63aacd8a69 | ||
|
c03ae6487d | ||
|
0c4f455f0c | ||
|
af3e08c446 | ||
|
975bfe7fac | ||
|
b075d948bb | ||
|
2651a64dbb | ||
|
fa7150cf2b | ||
|
6c4db980c9 | ||
|
7adaa510fd | ||
|
64ed7304f6 | ||
|
5aefaf061e | ||
|
724ee47c8b | ||
|
caaec6fcb1 | ||
|
15984b8fd9 | ||
|
2be913b077 | ||
|
34c11b0a0e | ||
|
64acdc29c3 | ||
|
598773c90d | ||
|
766238fd7e | ||
|
ac5242f173 | ||
|
215103cb33 | ||
|
ed3622d0a6 | ||
|
848fce5afa | ||
|
e37eeba0c9 | ||
|
7da49a4ccb | ||
|
7b02ace5e2 | ||
|
5fb67b32db | ||
|
a0c9ed2ace | ||
|
998b082a06 | ||
|
36d4156e2a | ||
|
74925ebba2 | ||
|
6f95b3395f | ||
|
dff00e2317 | ||
|
9a5d9d7ff5 | ||
|
5794c57898 | ||
|
35c713a26e | ||
|
6fb0620091 | ||
|
03df751bc7 | ||
|
c902858958 | ||
|
84fabdd5f4 | ||
|
f5cad15f84 | ||
|
dd3d0ae42c | ||
|
0b565852b8 | ||
|
ff13d7b2f7 | ||
|
7f5a89fa5a | ||
|
704e15f8c1 | ||
|
5eb1f13b54 | ||
|
176e05714e | ||
|
eefd84b02d | ||
|
af2742c34f | ||
|
2cf4acdb59 |
74 changed files with 4028 additions and 1122 deletions
37
.github/workflows/go.yml
vendored
Normal file
37
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.20.x
|
||||||
|
|
||||||
|
- name: Build linux
|
||||||
|
run: make linux
|
||||||
|
|
||||||
|
- name: Build macos
|
||||||
|
run: make macos
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: make lint
|
||||||
|
|
||||||
|
- name: retrieve all tags
|
||||||
|
run: git fetch --prune --unshallow --tags
|
||||||
|
|
||||||
|
- name: Print changes since last version
|
||||||
|
run: git log $(git describe --tags --abbrev=0)..HEAD --no-merges --oneline
|
62
.github/workflows/release.yml
vendored
Normal file
62
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*.*.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.20.x
|
||||||
|
|
||||||
|
- name: Build linux
|
||||||
|
run: make linux
|
||||||
|
|
||||||
|
- name: Build macos
|
||||||
|
run: make macos
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: make lint
|
||||||
|
|
||||||
|
- name: Zip macos
|
||||||
|
run: zip -r reflector_darwin_amd64.zip ./dist/darwin_amd64
|
||||||
|
|
||||||
|
- name: Zip linux
|
||||||
|
run: zip -r reflector_linux_amd64.zip ./dist/linux_amd64
|
||||||
|
|
||||||
|
- name: retrieve all tags
|
||||||
|
run: git fetch --prune --unshallow --tags
|
||||||
|
|
||||||
|
- name: Generate Changelog
|
||||||
|
run: git log $(git describe --tags --abbrev=0 @^)..@ --no-merges --oneline > ${{ github.workspace }}-CHANGELOG.txt
|
||||||
|
|
||||||
|
- name: upload to github releases
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
./reflector_linux_amd64.zip
|
||||||
|
./reflector_darwin_amd64.zip
|
||||||
|
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
||||||
|
|
||||||
|
# - name: Login to DockerHub
|
||||||
|
# uses: docker/login-action@v2
|
||||||
|
# with:
|
||||||
|
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Generate docker image
|
||||||
|
# run: make image
|
||||||
|
|
||||||
|
# - name: Docker push
|
||||||
|
# run: make publish_image
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
/vendor
|
/vendor
|
||||||
/config.json*
|
/config.json*
|
||||||
|
/dist
|
||||||
/bin
|
/bin
|
||||||
|
|
27
.travis.yml
27
.travis.yml
|
@ -1,12 +1,9 @@
|
||||||
os: linux
|
os: linux
|
||||||
dist: trusty
|
dist: bionic
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
env:
|
|
||||||
- GO111MODULE=on
|
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.15.x
|
- 1.20.x
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
|
@ -17,7 +14,7 @@ notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
||||||
# Skip the install step. Don't `go get` dependencies. Only build with the code in vendor/
|
# Skip the install step. Don't `go get` dependencies. Only build with the code in vendor/
|
||||||
install: true
|
#install: true
|
||||||
|
|
||||||
# Anything in before_script that returns a nonzero exit code will
|
# Anything in before_script that returns a nonzero exit code will
|
||||||
# flunk the build and immediately stop. It's sorta like having
|
# flunk the build and immediately stop. It's sorta like having
|
||||||
|
@ -25,14 +22,14 @@ install: true
|
||||||
before_script:
|
before_script:
|
||||||
# All the .go files, excluding vendor/ and model (auto generated)
|
# All the .go files, excluding vendor/ and model (auto generated)
|
||||||
- GO_FILES=$(find . -iname '*.go' ! -iname '*_test.go' -type f | grep -v /vendor/ ) #i wish we were this crazy :p
|
- GO_FILES=$(find . -iname '*.go' ! -iname '*_test.go' -type f | grep -v /vendor/ ) #i wish we were this crazy :p
|
||||||
- go get golang.org/x/tools/cmd/goimports # Used in build script for generated files
|
- go install golang.org/x/tools/cmd/goimports # Used in build script for generated files
|
||||||
# - go get github.com/golang/lint/golint # Linter
|
# - go get github.com/golang/lint/golint # Linter
|
||||||
# - go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter
|
# - go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter
|
||||||
- go get github.com/jgautheron/gocyclo # Check against high complexity
|
- go install github.com/fzipp/gocyclo/cmd/gocyclo@latest # Check against high complexity
|
||||||
- go get github.com/mdempsky/unconvert # Identifies unnecessary type conversions
|
- go install github.com/mdempsky/unconvert@latest # Identifies unnecessary type conversions
|
||||||
- go get github.com/kisielk/errcheck # Checks for unhandled errors
|
- go install github.com/kisielk/errcheck@latest # Checks for unhandled errors
|
||||||
- go get github.com/opennota/check/cmd/varcheck # Checks for unused vars
|
- go install github.com/opennota/check/cmd/varcheck@latest # Checks for unused vars
|
||||||
- go get github.com/opennota/check/cmd/structcheck # Checks for unused fields in structs
|
- go install github.com/opennota/check/cmd/structcheck@latest # Checks for unused fields in structs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +37,7 @@ before_script:
|
||||||
# in a modern Go project.
|
# in a modern Go project.
|
||||||
script:
|
script:
|
||||||
# Fail if a .go file hasn't been formatted with gofmt
|
# Fail if a .go file hasn't been formatted with gofmt
|
||||||
- test -z $(gofmt -s -l $GO_FILES)
|
- for i in $GO_FILES; do test -z $(gofmt -s -l $i); done
|
||||||
# Run unit tests
|
# Run unit tests
|
||||||
- make test
|
- make test
|
||||||
# Checks for unused vars and fields on structs
|
# Checks for unused vars and fields on structs
|
||||||
|
@ -59,11 +56,11 @@ script:
|
||||||
# one last linter - ignore autogen code
|
# one last linter - ignore autogen code
|
||||||
#- golint -set_exit_status $(go list ./... | grep -v /vendor/ )
|
#- golint -set_exit_status $(go list ./... | grep -v /vendor/ )
|
||||||
# Finally, build the binary
|
# Finally, build the binary
|
||||||
- make
|
- make linux
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
- provider: s3
|
- provider: s3
|
||||||
local_dir: ./bin
|
local_dir: ./dist/linux_amd64
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: lbryio/reflector.go
|
repo: lbryio/reflector.go
|
||||||
|
|
|
@ -3,7 +3,7 @@ EXPOSE 8080
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY bin/prism-bin ./prism
|
COPY dist/linux_amd64/prism-bin ./prism
|
||||||
RUN chmod +x prism
|
RUN chmod +x prism
|
||||||
|
|
||||||
ENTRYPOINT [ "/app/prism" ]
|
ENTRYPOINT [ "/app/prism" ]
|
||||||
|
|
44
Makefile
44
Makefile
|
@ -1,25 +1,33 @@
|
||||||
|
version := $(shell git describe --dirty --always --long --abbrev=7)
|
||||||
|
commit := $(shell git rev-parse --short HEAD)
|
||||||
|
commit_long := $(shell git rev-parse HEAD)
|
||||||
|
branch := $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
curTime := $(shell date +%s)
|
||||||
|
|
||||||
BINARY=prism-bin
|
BINARY=prism-bin
|
||||||
|
|
||||||
DIR = $(shell cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
|
|
||||||
BIN_DIR = ${DIR}/bin
|
|
||||||
IMPORT_PATH = github.com/lbryio/reflector.go
|
IMPORT_PATH = github.com/lbryio/reflector.go
|
||||||
|
LDFLAGS="-X ${IMPORT_PATH}/meta.version=$(version) -X ${IMPORT_PATH}/meta.commit=$(commit) -X ${IMPORT_PATH}/meta.commitLong=$(commit_long) -X ${IMPORT_PATH}/meta.branch=$(branch) -X '${IMPORT_PATH}/meta.Time=$(curTime)'"
|
||||||
|
DIR = $(shell cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
|
||||||
|
BIN_DIR = $(DIR)/dist
|
||||||
|
|
||||||
VERSION = $(shell git --git-dir=${DIR}/.git describe --dirty --always --long --abbrev=7)
|
.DEFAULT_GOAL := linux
|
||||||
LDFLAGS = -ldflags "-X ${IMPORT_PATH}/meta.Version=${VERSION} -X ${IMPORT_PATH}/meta.Time=$(shell date +%s)"
|
|
||||||
|
|
||||||
|
|
||||||
.PHONY: build clean test lint
|
|
||||||
.DEFAULT_GOAL: build
|
|
||||||
|
|
||||||
|
|
||||||
build:
|
|
||||||
mkdir -p ${BIN_DIR} && CGO_ENABLED=0 go build ${LDFLAGS} -asmflags -trimpath=${DIR} -o ${BIN_DIR}/${BINARY} main.go
|
|
||||||
|
|
||||||
clean:
|
|
||||||
if [ -f ${BIN_DIR}/${BINARY} ]; then rm ${BIN_DIR}/${BINARY}; fi
|
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
go test ./... -v -cover
|
go test -cover -v ./...
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
lint:
|
lint:
|
||||||
go get github.com/alecthomas/gometalinter && gometalinter --install && gometalinter ./...
|
./scripts/lint.sh
|
||||||
|
|
||||||
|
.PHONY: linux
|
||||||
|
linux:
|
||||||
|
GOARCH=amd64 GOOS=linux go build -ldflags ${LDFLAGS} -asmflags -trimpath=${DIR} -o ${BIN_DIR}/linux_amd64/${BINARY}
|
||||||
|
|
||||||
|
.PHONY: macos
|
||||||
|
macos:
|
||||||
|
GOARCH=amd64 GOOS=darwin go build -ldflags ${LDFLAGS} -asmflags -trimpath=${DIR} -o ${BIN_DIR}/darwin_amd64/${BINARY}
|
||||||
|
|
||||||
|
.PHONY: image
|
||||||
|
image:
|
||||||
|
docker buildx build -t lbry/reflector:$(version) -t lbry/reflector:latest --platform linux/amd64 .
|
|
@ -1,7 +1,7 @@
|
||||||
package cluster
|
package cluster
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io"
|
||||||
baselog "log"
|
baselog "log"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
@ -52,7 +52,7 @@ func (c *Cluster) Connect() error {
|
||||||
conf.MemberlistConfig.AdvertisePort = c.port
|
conf.MemberlistConfig.AdvertisePort = c.port
|
||||||
conf.NodeName = c.name
|
conf.NodeName = c.name
|
||||||
|
|
||||||
nullLogger := baselog.New(ioutil.Discard, "", 0)
|
nullLogger := baselog.New(io.Discard, "", 0)
|
||||||
conf.Logger = nullLogger
|
conf.Logger = nullLogger
|
||||||
|
|
||||||
c.eventCh = make(chan serf.Event)
|
c.eventCh = make(chan serf.Event)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/crypto"
|
"github.com/lbryio/lbry.go/v2/extras/crypto"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/cluster"
|
"github.com/lbryio/reflector.go/cluster"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"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/davecgh/go-spew/spew"
|
||||||
"github.com/golang/protobuf/jsonpb"
|
"github.com/gogo/protobuf/jsonpb"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeCmd(cmd *cobra.Command, args []string) {
|
func decodeCmd(cmd *cobra.Command, args []string) {
|
||||||
c, err := claim.DecodeClaimHex(args[0], "")
|
c, err := stake.DecodeClaimHex(args[0], "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/peer"
|
"github.com/lbryio/reflector.go/server/peer"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
@ -41,7 +41,7 @@ func getStreamCmd(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
var sd stream.SDBlob
|
var sd stream.SDBlob
|
||||||
|
|
||||||
sdb, err := s.Get(sdHash)
|
sdb, _, err := s.Get(sdHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ func getStreamCmd(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(sd.BlobInfos)-1; i++ {
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
93
cmd/integrity.go
Normal file
93
cmd/integrity.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/meta"
|
||||||
|
"github.com/lbryio/reflector.go/store/speedwalk"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
|
||||||
|
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.Fatalf("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 {
|
||||||
|
processedSoFar := atomic.AddInt32(processed, 1)
|
||||||
|
if worker == 0 {
|
||||||
|
remaining := int32(totalTasks) - processedSoFar
|
||||||
|
timePerBlob := time.Since(start).Microseconds() / int64(processedSoFar)
|
||||||
|
remainingTime := time.Duration(int64(remaining)*timePerBlob) * time.Microsecond
|
||||||
|
log.Infof("[T%d] %d/%d blobs processed so far. ETA: %s", worker, processedSoFar, totalTasks, remainingTime.String())
|
||||||
|
}
|
||||||
|
blobPath := path.Join(diskStorePath, b[:2], b)
|
||||||
|
blob, err := os.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
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/db"
|
"github.com/lbryio/reflector.go/db"
|
||||||
"github.com/lbryio/reflector.go/peer"
|
"github.com/lbryio/reflector.go/server/peer"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -33,11 +33,13 @@ func peerCmd(cmd *cobra.Command, args []string) {
|
||||||
peerServer := peer.NewServer(s3)
|
peerServer := peer.NewServer(s3)
|
||||||
|
|
||||||
if !peerNoDB {
|
if !peerNoDB {
|
||||||
db := new(db.SQL)
|
db := &db.SQL{
|
||||||
|
LogQueries: log.GetLevel() == log.DebugLevel,
|
||||||
|
}
|
||||||
err = db.Connect(globalConfig.DBConn)
|
err = db.Connect(globalConfig.DBConn)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
combo := store.NewDBBackedStore(s3, db)
|
combo := store.NewDBBackedStore(s3, db, false)
|
||||||
peerServer = peer.NewServer(combo)
|
peerServer = peer.NewServer(combo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
cmd/populatedb.go
Normal file
51
cmd/populatedb.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lbryio/reflector.go/db"
|
||||||
|
"github.com/lbryio/reflector.go/meta"
|
||||||
|
"github.com/lbryio/reflector.go/store/speedwalk"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = localDb.AddBlobs(blobs)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while storing to db: %s", errors.FullTrace(err))
|
||||||
|
}
|
||||||
|
}
|
370
cmd/reflector.go
370
cmd/reflector.go
|
@ -8,34 +8,63 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/util"
|
||||||
"github.com/lbryio/reflector.go/db"
|
"github.com/lbryio/reflector.go/db"
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
"github.com/lbryio/reflector.go/meta"
|
"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/reflector"
|
||||||
|
"github.com/lbryio/reflector.go/server/http"
|
||||||
|
"github.com/lbryio/reflector.go/server/http3"
|
||||||
|
"github.com/lbryio/reflector.go/server/peer"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||||
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cast"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tcpPeerPort int
|
//port configuration
|
||||||
http3PeerPort int
|
tcpPeerPort int
|
||||||
receiverPort int
|
http3PeerPort int
|
||||||
metricsPort int
|
httpPeerPort int
|
||||||
disableUploads bool
|
receiverPort int
|
||||||
disableBlocklist bool
|
metricsPort int
|
||||||
proxyAddress string
|
|
||||||
proxyPort string
|
//flags configuration
|
||||||
proxyProtocol string
|
disableUploads bool
|
||||||
useDB bool
|
disableBlocklist bool
|
||||||
cloudFrontEndpoint string
|
useDB bool
|
||||||
reflectorCmdDiskCache string
|
|
||||||
reflectorCmdMemCache int
|
//upstream configuration
|
||||||
|
upstreamReflector string
|
||||||
|
upstreamProtocol string
|
||||||
|
upstreamEdgeToken string
|
||||||
|
|
||||||
|
//downstream configuration
|
||||||
|
requestQueueSize int
|
||||||
|
|
||||||
|
//upstream edge configuration (to "cold" storage)
|
||||||
|
originEndpoint string
|
||||||
|
originEndpointFallback string
|
||||||
|
|
||||||
|
//cache configuration
|
||||||
|
diskCache string
|
||||||
|
secondaryDiskCache string
|
||||||
|
memCache int
|
||||||
)
|
)
|
||||||
|
var cacheManagers = []string{"localdb", "lfu", "arc", "lru", "simple"}
|
||||||
|
|
||||||
|
var cacheMangerToGcache = map[string]store.EvictionStrategy{
|
||||||
|
"lfu": store.LFU,
|
||||||
|
"arc": store.ARC,
|
||||||
|
"lru": store.LRU,
|
||||||
|
"simple": store.SIMPLE,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var cmd = &cobra.Command{
|
var cmd = &cobra.Command{
|
||||||
|
@ -43,20 +72,30 @@ func init() {
|
||||||
Short: "Run reflector server",
|
Short: "Run reflector server",
|
||||||
Run: reflectorCmd,
|
Run: reflectorCmd,
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&proxyAddress, "proxy-address", "", "address of another reflector server where blobs are fetched from")
|
|
||||||
cmd.Flags().StringVar(&proxyPort, "proxy-port", "5567", "port of another reflector server where blobs are fetched from")
|
cmd.Flags().IntVar(&tcpPeerPort, "tcp-peer-port", 5567, "The port reflector will distribute content from for the TCP (LBRY) protocol")
|
||||||
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().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(&http3PeerPort, "http3-peer-port", 5568, "The port reflector will distribute content from over HTTP3 protocol")
|
||||||
|
cmd.Flags().IntVar(&httpPeerPort, "http-peer-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(&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(&metricsPort, "metrics-port", 2112, "The port reflector will use for prometheus metrics")
|
||||||
|
|
||||||
cmd.Flags().BoolVar(&disableUploads, "disable-uploads", false, "Disable uploads to this reflector server")
|
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(&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().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'")
|
cmd.Flags().StringVar(&upstreamReflector, "upstream-reflector", "", "host:port of a reflector server where blobs are fetched from")
|
||||||
cmd.Flags().IntVar(&reflectorCmdMemCache, "mem-cache", 0, "enable in-memory cache with a max size of this many blobs")
|
cmd.Flags().StringVar(&upstreamProtocol, "upstream-protocol", "http", "protocol used to fetch blobs from another upstream reflector server (tcp/http3/http)")
|
||||||
|
cmd.Flags().StringVar(&upstreamEdgeToken, "upstream-edge-token", "", "token used to retrieve/authenticate protected content")
|
||||||
|
|
||||||
|
cmd.Flags().IntVar(&requestQueueSize, "request-queue-size", 200, "How many concurrent requests from downstream should be handled at once (the rest will wait)")
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&originEndpoint, "origin-endpoint", "", "HTTP edge endpoint for standard HTTP retrieval")
|
||||||
|
cmd.Flags().StringVar(&originEndpointFallback, "origin-endpoint-fallback", "", "HTTP edge endpoint for standard HTTP retrieval if first origin fails")
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&diskCache, "disk-cache", "100GB:/tmp/downloaded_blobs:localdb", "Where to cache blobs on the file system. format is 'sizeGB:CACHE_PATH:cachemanager' (cachemanagers: localdb/lfu/arc/lru)")
|
||||||
|
cmd.Flags().StringVar(&secondaryDiskCache, "optional-disk-cache", "", "Optional secondary file system cache for blobs. format is 'sizeGB:CACHE_PATH:cachemanager' (cachemanagers: localdb/lfu/arc/lru) (this would get hit before the one specified in disk-cache)")
|
||||||
|
cmd.Flags().IntVar(&memCache, "mem-cache", 0, "enable in-memory cache with a max size of this many blobs")
|
||||||
|
|
||||||
rootCmd.AddCommand(cmd)
|
rootCmd.AddCommand(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,11 +103,11 @@ func reflectorCmd(cmd *cobra.Command, args []string) {
|
||||||
log.Printf("reflector %s", meta.VersionString())
|
log.Printf("reflector %s", meta.VersionString())
|
||||||
|
|
||||||
// the blocklist logic requires the db backed store to be the outer-most store
|
// the blocklist logic requires the db backed store to be the outer-most store
|
||||||
underlyingStore := setupStore()
|
underlyingStore := initStores()
|
||||||
outerStore := wrapWithCache(underlyingStore)
|
underlyingStoreWithCaches, cleanerStopper := initCaches(underlyingStore)
|
||||||
|
|
||||||
if !disableUploads {
|
if !disableUploads {
|
||||||
reflectorServer := reflector.NewServer(underlyingStore)
|
reflectorServer := reflector.NewServer(underlyingStore, underlyingStoreWithCaches)
|
||||||
reflectorServer.Timeout = 3 * time.Minute
|
reflectorServer.Timeout = 3 * time.Minute
|
||||||
reflectorServer.EnableBlocklist = !disableBlocklist
|
reflectorServer.EnableBlocklist = !disableBlocklist
|
||||||
|
|
||||||
|
@ -79,117 +118,264 @@ func reflectorCmd(cmd *cobra.Command, args []string) {
|
||||||
defer reflectorServer.Shutdown()
|
defer reflectorServer.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
peerServer := peer.NewServer(outerStore)
|
peerServer := peer.NewServer(underlyingStoreWithCaches)
|
||||||
err := peerServer.Start(":" + strconv.Itoa(tcpPeerPort))
|
err := peerServer.Start(":" + strconv.Itoa(tcpPeerPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer peerServer.Shutdown()
|
defer peerServer.Shutdown()
|
||||||
|
|
||||||
http3PeerServer := http3.NewServer(outerStore)
|
http3PeerServer := http3.NewServer(underlyingStoreWithCaches, requestQueueSize)
|
||||||
err = http3PeerServer.Start(":" + strconv.Itoa(http3PeerPort))
|
err = http3PeerServer.Start(":" + strconv.Itoa(http3PeerPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer http3PeerServer.Shutdown()
|
defer http3PeerServer.Shutdown()
|
||||||
|
|
||||||
|
httpServer := http.NewServer(store.WithSingleFlight("sf-http", underlyingStoreWithCaches), requestQueueSize, upstreamEdgeToken)
|
||||||
|
err = httpServer.Start(":" + strconv.Itoa(httpPeerPort))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer httpServer.Shutdown()
|
||||||
|
|
||||||
metricsServer := metrics.NewServer(":"+strconv.Itoa(metricsPort), "/metrics")
|
metricsServer := metrics.NewServer(":"+strconv.Itoa(metricsPort), "/metrics")
|
||||||
metricsServer.Start()
|
metricsServer.Start()
|
||||||
defer metricsServer.Shutdown()
|
defer metricsServer.Shutdown()
|
||||||
|
defer underlyingStoreWithCaches.Shutdown()
|
||||||
|
defer underlyingStore.Shutdown() //do we actually need this? Oo
|
||||||
|
|
||||||
interruptChan := make(chan os.Signal, 1)
|
interruptChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(interruptChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(interruptChan, os.Interrupt, syscall.SIGTERM)
|
||||||
<-interruptChan
|
<-interruptChan
|
||||||
// deferred shutdowns happen now
|
// deferred shutdowns happen now
|
||||||
|
cleanerStopper.StopAndWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupStore() store.BlobStore {
|
func initUpstreamStore() store.BlobStore {
|
||||||
|
var s store.BlobStore
|
||||||
|
if upstreamReflector == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch upstreamProtocol {
|
||||||
|
case "tcp":
|
||||||
|
s = peer.NewStore(peer.StoreOpts{
|
||||||
|
Address: upstreamReflector,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
})
|
||||||
|
case "http3":
|
||||||
|
s = http3.NewStore(http3.StoreOpts{
|
||||||
|
Address: upstreamReflector,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
})
|
||||||
|
case "http":
|
||||||
|
s = store.NewHttpStore(upstreamReflector, upstreamEdgeToken)
|
||||||
|
default:
|
||||||
|
log.Fatalf("protocol is not recognized: %s", upstreamProtocol)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
func initEdgeStore() store.BlobStore {
|
||||||
|
var s3Store *store.S3Store
|
||||||
var s store.BlobStore
|
var s store.BlobStore
|
||||||
|
|
||||||
if proxyAddress != "" {
|
if conf != "none" {
|
||||||
switch proxyProtocol {
|
s3Store = store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName)
|
||||||
case "tcp":
|
}
|
||||||
s = peer.NewStore(peer.StoreOpts{
|
if originEndpointFallback != "" && originEndpoint != "" {
|
||||||
Address: proxyAddress + ":" + proxyPort,
|
ittt := store.NewITTTStore(store.NewCloudFrontROStore(originEndpoint), store.NewCloudFrontROStore(originEndpointFallback))
|
||||||
Timeout: 30 * time.Second,
|
if s3Store != nil {
|
||||||
})
|
s = store.NewCloudFrontRWStore(ittt, s3Store)
|
||||||
case "http3":
|
|
||||||
s = http3.NewStore(http3.StoreOpts{
|
|
||||||
Address: proxyAddress + ":" + proxyPort,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
})
|
|
||||||
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 {
|
} else {
|
||||||
s = s3Store
|
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)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s = store.NewDBBackedStore(s, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapWithCache(s store.BlobStore) store.BlobStore {
|
func initDBStore(s store.BlobStore) store.BlobStore {
|
||||||
wrapped := s
|
if useDB {
|
||||||
|
dbInst := &db.SQL{
|
||||||
diskCacheMaxSize, diskCachePath := diskCacheParams()
|
TrackAccess: db.TrackAccessStreams,
|
||||||
if diskCacheMaxSize > 0 {
|
LogQueries: log.GetLevel() == log.DebugLevel,
|
||||||
err := os.MkdirAll(diskCachePath, os.ModePerm)
|
}
|
||||||
|
err := dbInst.Connect(globalConfig.DBConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
wrapped = store.NewCachingStore(
|
s = store.NewDBBackedStore(s, dbInst, false)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func initStores() store.BlobStore {
|
||||||
|
s := initUpstreamStore()
|
||||||
|
if s == nil {
|
||||||
|
s = initEdgeStore()
|
||||||
|
}
|
||||||
|
s = initDBStore(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// initCaches returns a store wrapped with caches and a stop group to execute a clean shutdown
|
||||||
|
func initCaches(s store.BlobStore) (store.BlobStore, *stop.Group) {
|
||||||
|
stopper := stop.New()
|
||||||
|
diskStore := initDiskStore(s, diskCache, stopper)
|
||||||
|
finalStore := initDiskStore(diskStore, secondaryDiskCache, stopper)
|
||||||
|
stop.New()
|
||||||
|
if memCache > 0 {
|
||||||
|
finalStore = store.NewCachingStore(
|
||||||
"reflector",
|
"reflector",
|
||||||
wrapped,
|
finalStore,
|
||||||
store.NewLRUStore("peer_server", store.NewDiskStore(diskCachePath, 2), diskCacheMaxSize),
|
store.NewGcacheStore("mem", store.NewMemStore(), memCache, store.LRU),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return finalStore, stopper
|
||||||
|
}
|
||||||
|
|
||||||
if reflectorCmdMemCache > 0 {
|
func initDiskStore(upstreamStore store.BlobStore, diskParams string, stopper *stop.Group) store.BlobStore {
|
||||||
wrapped = store.NewCachingStore(
|
diskCacheMaxSize, diskCachePath, cacheManager := diskCacheParams(diskParams)
|
||||||
"reflector",
|
//we are tracking blobs in memory with a 1 byte long boolean, which means that for each 2MB (a blob) we need 1Byte
|
||||||
wrapped,
|
// so if the underlying cache holds 10MB, 10MB/2MB=5Bytes which is also the exact count of objects to restore on startup
|
||||||
store.NewLRUStore("peer_server", store.NewMemStore(), reflectorCmdMemCache),
|
realCacheSize := float64(diskCacheMaxSize) / float64(stream.MaxBlobSize)
|
||||||
)
|
if diskCacheMaxSize == 0 {
|
||||||
|
return upstreamStore
|
||||||
|
}
|
||||||
|
err := os.MkdirAll(diskCachePath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diskStore := store.NewDiskStore(diskCachePath, 2)
|
||||||
|
var unwrappedStore store.BlobStore
|
||||||
|
cleanerStopper := stop.New(stopper)
|
||||||
|
|
||||||
|
if cacheManager == "localdb" {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
unwrappedStore = store.NewDBBackedStore(diskStore, localDb, true)
|
||||||
|
go cleanOldestBlobs(int(realCacheSize), localDb, unwrappedStore, cleanerStopper)
|
||||||
|
} else {
|
||||||
|
unwrappedStore = store.NewGcacheStore("nvme", store.NewDiskStore(diskCachePath, 2), int(realCacheSize), cacheMangerToGcache[cacheManager])
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped := store.NewCachingStore(
|
||||||
|
"reflector",
|
||||||
|
upstreamStore,
|
||||||
|
unwrappedStore,
|
||||||
|
)
|
||||||
return wrapped
|
return wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
func diskCacheParams() (int, string) {
|
func diskCacheParams(diskParams string) (int, string, string) {
|
||||||
if reflectorCmdDiskCache == "" {
|
if diskParams == "" {
|
||||||
return 0, ""
|
return 0, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(reflectorCmdDiskCache, ":")
|
parts := strings.Split(diskParams, ":")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 3 {
|
||||||
log.Fatalf("--disk-cache must be a number, followed by ':', followed by a string")
|
log.Fatalf("%s does is formatted incorrectly. Expected format: 'sizeGB:CACHE_PATH:cachemanager' for example: '100GB:/tmp/downloaded_blobs:localdb'", diskParams)
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
path := parts[1]
|
||||||
|
cacheManager := parts[2]
|
||||||
|
|
||||||
if len(path) == 0 || path[0] != '/' {
|
if len(path) == 0 || path[0] != '/' {
|
||||||
log.Fatalf("--disk-cache path must start with '/'")
|
log.Fatalf("disk cache paths must start with '/'")
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxSize, path
|
if !util.InSlice(cacheManager, cacheManagers) {
|
||||||
|
log.Fatalf("specified cache manager '%s' is not supported. Use one of the following: %v", cacheManager, cacheManagers)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, cacheManager
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
15
cmd/root.go
15
cmd/root.go
|
@ -2,14 +2,14 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/updater"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht"
|
"github.com/lbryio/lbry.go/v2/dht"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/util"
|
"github.com/lbryio/lbry.go/v2/extras/util"
|
||||||
"github.com/lbryio/reflector.go/updater"
|
|
||||||
|
|
||||||
"github.com/johntdyer/slackrus"
|
"github.com/johntdyer/slackrus"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -24,6 +24,7 @@ type Config struct {
|
||||||
BucketName string `json:"bucket_name"`
|
BucketName string `json:"bucket_name"`
|
||||||
DBConn string `json:"db_conn"`
|
DBConn string `json:"db_conn"`
|
||||||
SlackHookURL string `json:"slack_hook_url"`
|
SlackHookURL string `json:"slack_hook_url"`
|
||||||
|
SlackChannel string `json:"slack_channel"`
|
||||||
UpdateBinURL string `json:"update_bin_url"`
|
UpdateBinURL string `json:"update_bin_url"`
|
||||||
UpdateCmd string `json:"update_cmd"`
|
UpdateCmd string `json:"update_cmd"`
|
||||||
}
|
}
|
||||||
|
@ -101,7 +102,7 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||||
hook := &slackrus.SlackrusHook{
|
hook := &slackrus.SlackrusHook{
|
||||||
HookURL: globalConfig.SlackHookURL,
|
HookURL: globalConfig.SlackHookURL,
|
||||||
AcceptedLevels: slackrus.LevelThreshold(logrus.InfoLevel),
|
AcceptedLevels: slackrus.LevelThreshold(logrus.InfoLevel),
|
||||||
Channel: "#reflector-logs",
|
Channel: globalConfig.SlackChannel,
|
||||||
//IconEmoji: ":ghost:",
|
//IconEmoji: ":ghost:",
|
||||||
//Username: "reflector.go",
|
//Username: "reflector.go",
|
||||||
}
|
}
|
||||||
|
@ -140,7 +141,7 @@ func argFuncs(funcs ...cobra.PositionalArgs) cobra.PositionalArgs {
|
||||||
func loadConfig(path string) (Config, error) {
|
func loadConfig(path string) (Config, error) {
|
||||||
var c Config
|
var c Config
|
||||||
|
|
||||||
raw, err := ioutil.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return c, errors.Err("config file not found")
|
return c, errors.Err("config file not found")
|
||||||
|
@ -163,3 +164,9 @@ func mustGetFlagInt64(cmd *cobra.Command, name string) int64 {
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//func mustGetFlagBool(cmd *cobra.Command, name string) bool {
|
||||||
|
// v, err := cmd.Flags().GetBool(name)
|
||||||
|
// checkErr(err)
|
||||||
|
// return v
|
||||||
|
//}
|
||||||
|
|
158
cmd/send.go
Normal file
158
cmd/send.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"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 func() { _ = hackyReflector.Close() }()
|
||||||
|
|
||||||
|
filePath := args[1]
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
checkErr(err)
|
||||||
|
defer func() { _ = 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 := os.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 = os.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
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/reflector"
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
|
@ -52,9 +51,8 @@ func sendBlobCmd(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
data, err := ioutil.ReadAll(file)
|
defer func() { _ = file.Close() }()
|
||||||
checkErr(err)
|
s, err := stream.New(file)
|
||||||
s, err := stream.New(data)
|
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
sdBlob := &stream.SDBlob{}
|
sdBlob := &stream.SDBlob{}
|
||||||
|
|
13
cmd/start.go
13
cmd/start.go
|
@ -7,15 +7,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht"
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
|
||||||
"github.com/lbryio/reflector.go/cluster"
|
"github.com/lbryio/reflector.go/cluster"
|
||||||
"github.com/lbryio/reflector.go/db"
|
"github.com/lbryio/reflector.go/db"
|
||||||
"github.com/lbryio/reflector.go/peer"
|
|
||||||
"github.com/lbryio/reflector.go/prism"
|
"github.com/lbryio/reflector.go/prism"
|
||||||
"github.com/lbryio/reflector.go/reflector"
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
|
"github.com/lbryio/reflector.go/server/peer"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/dht"
|
||||||
|
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -52,11 +53,13 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCmd(cmd *cobra.Command, args []string) {
|
func startCmd(cmd *cobra.Command, args []string) {
|
||||||
db := new(db.SQL)
|
db := &db.SQL{
|
||||||
|
LogQueries: log.GetLevel() == log.DebugLevel,
|
||||||
|
}
|
||||||
err := db.Connect(globalConfig.DBConn)
|
err := db.Connect(globalConfig.DBConn)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
s3 := store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName)
|
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()
|
conf := prism.DefaultConf()
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/meta"
|
"github.com/lbryio/reflector.go/meta"
|
||||||
"github.com/lbryio/reflector.go/peer"
|
|
||||||
"github.com/lbryio/reflector.go/reflector"
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
|
"github.com/lbryio/reflector.go/server/peer"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -31,7 +31,7 @@ func testCmd(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
memStore := store.NewMemStore()
|
memStore := store.NewMemStore()
|
||||||
|
|
||||||
reflectorServer := reflector.NewServer(memStore)
|
reflectorServer := reflector.NewServer(memStore, memStore)
|
||||||
reflectorServer.Timeout = 3 * time.Minute
|
reflectorServer.Timeout = 3 * time.Minute
|
||||||
|
|
||||||
err := reflectorServer.Start(":" + strconv.Itoa(reflector.DefaultPort))
|
err := reflectorServer.Start(":" + strconv.Itoa(reflector.DefaultPort))
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/lbryio/reflector.go/reflector"
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,13 +31,15 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadCmd(cmd *cobra.Command, args []string) {
|
func uploadCmd(cmd *cobra.Command, args []string) {
|
||||||
db := new(db.SQL)
|
db := &db.SQL{
|
||||||
|
LogQueries: log.GetLevel() == log.DebugLevel,
|
||||||
|
}
|
||||||
err := db.Connect(globalConfig.DBConn)
|
err := db.Connect(globalConfig.DBConn)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
st := store.NewDBBackedStore(
|
st := store.NewDBBackedStore(
|
||||||
store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName),
|
store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName),
|
||||||
db)
|
db, false)
|
||||||
|
|
||||||
uploader := reflector.NewUploader(db, st, uploadWorkers, uploadSkipExistsCheck, uploadDeleteBlobsAfterUpload)
|
uploader := reflector.NewUploader(db, st, uploadWorkers, uploadSkipExistsCheck, uploadDeleteBlobsAfterUpload)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/meta"
|
"github.com/lbryio/reflector.go/meta"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,5 +18,5 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func versionCmd(cmd *cobra.Command, args []string) {
|
func versionCmd(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println(meta.VersionString())
|
fmt.Println(meta.FullName())
|
||||||
}
|
}
|
||||||
|
|
307
db/db.go
307
db/db.go
|
@ -3,16 +3,22 @@ package db
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
qt "github.com/lbryio/lbry.go/v2/extras/query"
|
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"
|
||||||
_ "github.com/go-sql-driver/mysql" // blank import for db driver ensures its imported even if its not used
|
_ "github.com/go-sql-driver/mysql" // blank import for db driver ensures its imported even if its not used
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/volatiletech/null"
|
"github.com/volatiletech/null/v8"
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SdBlob is a special blob that contains information on the rest of the blobs in the stream
|
// SdBlob is a special blob that contains information on the rest of the blobs in the stream
|
||||||
|
@ -30,19 +36,38 @@ type SdBlob struct {
|
||||||
StreamHash string `json:"stream_hash"`
|
StreamHash string `json:"stream_hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type trackAccess int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TrackAccessNone trackAccess = iota // Don't track accesses
|
||||||
|
TrackAccessStreams // Track accesses at the stream level
|
||||||
|
TrackAccessBlobs // Track accesses at the blob level
|
||||||
|
)
|
||||||
|
|
||||||
// SQL implements the DB interface
|
// SQL implements the DB interface
|
||||||
type SQL struct {
|
type SQL struct {
|
||||||
conn *sql.DB
|
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{}) {
|
func (s SQL) logQuery(query string, args ...interface{}) {
|
||||||
s, err := qt.InterpolateParams(query, args...)
|
if !s.LogQueries {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qStr, err := qt.InterpolateParams(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
} else {
|
} else {
|
||||||
log.Debugln(s)
|
log.Debugln(qStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,16 +98,97 @@ func (s *SQL) AddBlob(hash string, length int, isStored bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//AddBlobs adds blobs to the database.
|
||||||
|
func (s *SQL) AddBlobs(hash []string) error {
|
||||||
|
if s.conn == nil {
|
||||||
|
return errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
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(q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQL) insertBlob(hash string, length int, isStored bool) (int64, error) {
|
func (s *SQL) insertBlob(hash string, length int, isStored bool) (int64, error) {
|
||||||
if length <= 0 {
|
if length <= 0 {
|
||||||
return 0, errors.Err("length must be positive")
|
return 0, errors.Err("length must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []interface{}{hash, isStored, length}
|
var (
|
||||||
blobID, err := s.exec(
|
q string
|
||||||
"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 []interface{}
|
||||||
args...,
|
|
||||||
)
|
)
|
||||||
|
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(q, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -95,17 +201,33 @@ func (s *SQL) insertBlob(hash string, length int, isStored bool) (int64, error)
|
||||||
if blobID == 0 {
|
if blobID == 0 {
|
||||||
return 0, errors.Err("blob ID is 0 even after INSERTing and SELECTing")
|
return 0, errors.Err("blob ID is 0 even after INSERTing and SELECTing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.TrackAccess == TrackAccessBlobs {
|
||||||
|
err := s.touchBlobs([]uint64{uint64(blobID)})
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return blobID, nil
|
return blobID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQL) insertStream(hash string, sdBlobID int64) (int64, error) {
|
func (s *SQL) insertStream(hash string, sdBlobID int64) (int64, error) {
|
||||||
args := []interface{}{hash, sdBlobID, time.Now()}
|
var (
|
||||||
streamID, err := s.exec(
|
q string
|
||||||
"INSERT IGNORE INTO stream (hash, sd_blob_id, last_accessed_at) VALUES ("+qt.Qs(len(args))+")",
|
args []interface{}
|
||||||
args...,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(q, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.Err(err)
|
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")
|
return 0, errors.Err("stream ID is 0 even after INSERTing and SELECTing")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.TrackAccessTime {
|
if s.TrackAccess == TrackAccessStreams {
|
||||||
err := s.touch([]uint64{uint64(streamID)})
|
err := s.touchStreams([]uint64{uint64(streamID)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.Err(err)
|
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.
|
// HasBlob checks if the database contains the blob information.
|
||||||
func (s *SQL) HasBlob(hash string) (bool, error) {
|
func (s *SQL) HasBlob(hash string, touch bool) (bool, error) {
|
||||||
exists, err := s.HasBlobs([]string{hash})
|
exists, err := s.HasBlobs([]string{hash}, touch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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.
|
// 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) {
|
func (s *SQL) HasBlobs(hashes []string, touch bool) (map[string]bool, error) {
|
||||||
exists, streamsNeedingTouch, err := s.hasBlobs(hashes)
|
exists, idsNeedingTouch, err := s.hasBlobs(hashes)
|
||||||
s.touch(streamsNeedingTouch)
|
|
||||||
|
if touch {
|
||||||
|
if s.TrackAccess == TrackAccessBlobs {
|
||||||
|
_ = s.touchBlobs(idsNeedingTouch)
|
||||||
|
} else if s.TrackAccess == TrackAccessStreams {
|
||||||
|
_ = s.touchStreams(idsNeedingTouch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return exists, err
|
return exists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQL) touch(streamIDs []uint64) error {
|
func (s *SQL) touchBlobs(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(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 {
|
if len(streamIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -159,7 +307,7 @@ func (s *SQL) touch(streamIDs []uint64) error {
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
_, err := s.exec(query, args...)
|
_, err := s.exec(query, args...)
|
||||||
log.Debugf("stream access query touched %d streams and took %s", len(streamIDs), time.Since(startTime))
|
log.Debugf("touched %d streams and took %s", len(streamIDs), time.Since(startTime))
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,14 +318,15 @@ func (s *SQL) hasBlobs(hashes []string) (map[string]bool, []uint64, error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
hash string
|
hash string
|
||||||
streamID uint64
|
blobID uint64
|
||||||
|
streamID null.Uint64
|
||||||
lastAccessedAt null.Time
|
lastAccessedAt null.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
var needsTouch []uint64
|
var needsTouch []uint64
|
||||||
exists := make(map[string]bool)
|
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
|
maxBatchSize := 10000
|
||||||
doneIndex := 0
|
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))
|
log.Debugf("getting hashes[%d:%d] of %d", doneIndex, sliceEnd, len(hashes))
|
||||||
batch := hashes[doneIndex:sliceEnd]
|
batch := hashes[doneIndex:sliceEnd]
|
||||||
|
|
||||||
// TODO: this query doesn't work for SD blobs, which are not in the stream_blob table
|
var query string
|
||||||
|
if s.TrackAccess == TrackAccessBlobs {
|
||||||
query := `SELECT b.hash, s.id, s.last_accessed_at
|
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
|
FROM blob_ b
|
||||||
LEFT JOIN stream_blob sb ON b.id = sb.blob_id
|
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)
|
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)) + `)`
|
WHERE b.is_stored = 1 and b.hash IN (` + qt.Qs(len(batch)) + `)`
|
||||||
args := make([]interface{}, len(batch)+1)
|
} else {
|
||||||
args[0] = true
|
query = `SELECT b.hash, b.id, NULL, NULL
|
||||||
for i := range batch {
|
FROM blob_ b
|
||||||
args[i+1] = batch[i]
|
WHERE b.is_stored = 1 and b.hash IN (` + qt.Qs(len(batch)) + `)`
|
||||||
}
|
}
|
||||||
|
|
||||||
logQuery(query, args...)
|
args := make([]interface{}, len(batch))
|
||||||
|
for i := range batch {
|
||||||
|
args[i] = batch[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logQuery(query, args...)
|
||||||
|
|
||||||
err := func() error {
|
err := func() error {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
@ -214,13 +372,17 @@ WHERE b.is_stored = ? and b.hash IN (` + qt.Qs(len(batch)) + `)`
|
||||||
defer closeRows(rows)
|
defer closeRows(rows)
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err := rows.Scan(&hash, &streamID, &lastAccessedAt)
|
err := rows.Scan(&hash, &blobID, &streamID, &lastAccessedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
}
|
}
|
||||||
exists[hash] = true
|
exists[hash] = true
|
||||||
if s.TrackAccessTime && (!lastAccessedAt.Valid || lastAccessedAt.Time.Before(touchDeadline)) {
|
if !lastAccessedAt.Valid || lastAccessedAt.Time.Before(touchDeadline) {
|
||||||
needsTouch = append(needsTouch, streamID)
|
if s.TrackAccess == TrackAccessBlobs {
|
||||||
|
needsTouch = append(needsTouch, blobID)
|
||||||
|
} else if s.TrackAccess == TrackAccessStreams && !streamID.IsZero() {
|
||||||
|
needsTouch = append(needsTouch, streamID.Uint64)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,8 +402,14 @@ WHERE b.is_stored = ? and b.hash IN (` + qt.Qs(len(batch)) + `)`
|
||||||
return exists, needsTouch, nil
|
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 {
|
func (s *SQL) Delete(hash string) error {
|
||||||
|
if s.SoftDelete {
|
||||||
|
_, err := s.exec("UPDATE blob_ SET is_stored = 0 WHERE hash = ?", hash)
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err := s.exec("DELETE FROM stream WHERE sd_blob_id = (SELECT id FROM blob_ WHERE hash = ?)", hash)
|
_, err := s.exec("DELETE FROM stream WHERE sd_blob_id = (SELECT id FROM blob_ WHERE hash = ?)", hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
|
@ -251,11 +419,59 @@ func (s *SQL) Delete(hash string) error {
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//LeastRecentlyAccessedHashes gets the least recently accessed blobs
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Block will mark a blob as blocked
|
||||||
func (s *SQL) Block(hash string) error {
|
func (s *SQL) Block(hash string) error {
|
||||||
query := "INSERT IGNORE INTO blocked SET hash = ?"
|
query := "INSERT IGNORE INTO blocked SET hash = ?"
|
||||||
args := []interface{}{hash}
|
args := []interface{}{hash}
|
||||||
logQuery(query, args...)
|
s.logQuery(query, args...)
|
||||||
_, err := s.conn.Exec(query, args...)
|
_, err := s.conn.Exec(query, args...)
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
}
|
}
|
||||||
|
@ -263,7 +479,7 @@ func (s *SQL) Block(hash string) error {
|
||||||
// GetBlocked will return a list of blocked hashes
|
// GetBlocked will return a list of blocked hashes
|
||||||
func (s *SQL) GetBlocked() (map[string]bool, error) {
|
func (s *SQL) GetBlocked() (map[string]bool, error) {
|
||||||
query := "SELECT hash FROM blocked"
|
query := "SELECT hash FROM blocked"
|
||||||
logQuery(query)
|
s.logQuery(query)
|
||||||
rows, err := s.conn.Query(query)
|
rows, err := s.conn.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Err(err)
|
return nil, errors.Err(err)
|
||||||
|
@ -306,7 +522,7 @@ func (s *SQL) MissingBlobsForKnownStream(sdHash string) ([]string, error) {
|
||||||
`
|
`
|
||||||
args := []interface{}{sdHash}
|
args := []interface{}{sdHash}
|
||||||
|
|
||||||
logQuery(query, args...)
|
s.logQuery(query, args...)
|
||||||
|
|
||||||
rows, err := s.conn.Query(query, args...)
|
rows, err := s.conn.Query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -385,7 +601,7 @@ func (s *SQL) GetHashRange() (string, string, error) {
|
||||||
|
|
||||||
query := "SELECT MIN(hash), MAX(hash) from blob_"
|
query := "SELECT MIN(hash), MAX(hash) from blob_"
|
||||||
|
|
||||||
logQuery(query)
|
s.logQuery(query)
|
||||||
|
|
||||||
err := s.conn.QueryRow(query).Scan(&min, &max)
|
err := s.conn.QueryRow(query).Scan(&min, &max)
|
||||||
return min, max, err
|
return min, max, err
|
||||||
|
@ -409,7 +625,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"
|
query := "SELECT hash FROM blob_ WHERE hash >= ? AND hash <= ? AND is_stored = 1"
|
||||||
args := []interface{}{start.Hex(), end.Hex()}
|
args := []interface{}{start.Hex(), end.Hex()}
|
||||||
|
|
||||||
logQuery(query, args...)
|
s.logQuery(query, args...)
|
||||||
|
|
||||||
rows, err := s.conn.Query(query, args...)
|
rows, err := s.conn.Query(query, args...)
|
||||||
defer closeRows(rows)
|
defer closeRows(rows)
|
||||||
|
@ -490,7 +706,7 @@ func closeRows(rows *sql.Rows) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQL) exec(query string, args ...interface{}) (int64, error) {
|
func (s *SQL) exec(query string, args ...interface{}) (int64, error) {
|
||||||
logQuery(query, args...)
|
s.logQuery(query, args...)
|
||||||
attempt, maxAttempts := 0, 3
|
attempt, maxAttempts := 0, 3
|
||||||
Retry:
|
Retry:
|
||||||
attempt++
|
attempt++
|
||||||
|
@ -528,8 +744,11 @@ CREATE TABLE blob_ (
|
||||||
hash char(96) NOT NULL,
|
hash char(96) NOT NULL,
|
||||||
is_stored TINYINT(1) NOT NULL DEFAULT 0,
|
is_stored TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
length bigint(20) unsigned DEFAULT NULL,
|
length bigint(20) unsigned DEFAULT NULL,
|
||||||
|
last_accessed_at TIMESTAMP NULL DEFAULT NULL,
|
||||||
PRIMARY KEY (id),
|
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 (
|
CREATE TABLE stream (
|
||||||
|
|
148
go.mod
148
go.mod
|
@ -1,46 +1,124 @@
|
||||||
module github.com/lbryio/reflector.go
|
module github.com/lbryio/reflector.go
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
replace github.com/btcsuite/btcd => github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19
|
replace github.com/btcsuite/btcd => github.com/lbryio/lbrycrd.go v0.0.0-20200203050410-e1076f12bf19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect
|
github.com/aws/aws-sdk-go v1.45.24
|
||||||
github.com/aws/aws-sdk-go v1.16.11
|
github.com/bluele/gcache v0.0.2
|
||||||
|
github.com/brk0v/directio v0.0.0-20190225130936-69406e757cf7
|
||||||
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
|
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
|
||||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
|
||||||
|
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/go-sql-driver/mysql v1.4.1
|
github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db
|
||||||
github.com/golang/protobuf v1.4.2
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/google/btree v1.0.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.1
|
||||||
github.com/google/gops v0.3.7
|
github.com/gogo/protobuf v1.3.2
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/golang/protobuf v1.5.3
|
||||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
github.com/google/gops v0.3.28
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/hashicorp/memberlist v0.1.4 // indirect
|
github.com/hashicorp/serf v0.10.1
|
||||||
github.com/hashicorp/serf v0.8.2
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
||||||
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07
|
github.com/johntdyer/slackrus v0.0.0-20230315191314-80bc92dee4fc
|
||||||
github.com/karrick/godirwalk v1.16.1
|
github.com/karrick/godirwalk v1.17.0
|
||||||
github.com/lbryio/chainquery v1.9.0
|
github.com/lbryio/chainquery v1.9.1-0.20230515181855-2fcba3115cfe
|
||||||
github.com/lbryio/lbry.go v1.1.2 // indirect
|
github.com/lbryio/lbry.go/v2 v2.7.2-0.20230307181431-a01aa6dc0629
|
||||||
github.com/lbryio/lbry.go/v2 v2.6.1-0.20200901175808-73382bb02128
|
github.com/lbryio/types v0.0.0-20220224142228-73610f6654a6
|
||||||
github.com/lbryio/types v0.0.0-20191228214437-05a22073b4ec
|
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||||
github.com/lucas-clemente/quic-go v0.18.1
|
github.com/prometheus/client_golang v1.16.0
|
||||||
github.com/phayes/freeport v0.0.0-20171002185219-e27662a4a9d6
|
github.com/quic-go/quic-go v0.39.0
|
||||||
github.com/prometheus/client_golang v0.9.2
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/sirupsen/logrus v1.4.2
|
github.com/spf13/cast v1.5.1
|
||||||
github.com/spf13/afero v1.4.1
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/cast v1.3.0
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/spf13/cobra v0.0.3
|
github.com/volatiletech/null/v8 v8.1.2
|
||||||
github.com/spf13/pflag v1.0.3 // indirect
|
go.uber.org/atomic v1.11.0
|
||||||
github.com/stretchr/testify v1.4.0
|
golang.org/x/sync v0.4.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/tools v0.0.0-20191227053925-7b8e75db28f4 // indirect
|
|
||||||
google.golang.org/appengine v1.6.2 // indirect
|
|
||||||
gotest.tools v2.2.0+incompatible
|
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.15
|
require (
|
||||||
|
github.com/armon/go-metrics v0.4.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||||
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/friendsofgo/errors v0.9.2 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-errors/errors v1.4.2 // indirect
|
||||||
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||||
|
github.com/google/btree v1.0.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
|
github.com/gorilla/rpc v1.2.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/memberlist v0.5.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/johntdyer/slack-go v0.0.0-20230314151037-c5bf334f9b6e // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
|
github.com/miekg/dns v1.1.41 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.10.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||||
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
|
github.com/slack-go/slack v0.12.1 // indirect
|
||||||
|
github.com/spf13/afero v1.9.3 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/spf13/viper v1.15.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
github.com/volatiletech/inflect v0.0.1 // indirect
|
||||||
|
github.com/volatiletech/randomize v0.0.1 // indirect
|
||||||
|
github.com/volatiletech/strmangle v0.0.4 // indirect
|
||||||
|
go.uber.org/mock v0.3.0 // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||||
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
|
golang.org/x/net v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
|
golang.org/x/text v0.9.0 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
|
@ -60,6 +60,7 @@ func (s *Server) Shutdown() {
|
||||||
const (
|
const (
|
||||||
ns = "reflector"
|
ns = "reflector"
|
||||||
subsystemCache = "cache"
|
subsystemCache = "cache"
|
||||||
|
subsystemITTT = "ittt"
|
||||||
|
|
||||||
labelDirection = "direction"
|
labelDirection = "direction"
|
||||||
labelErrorType = "error_type"
|
labelErrorType = "error_type"
|
||||||
|
@ -85,8 +86,13 @@ const (
|
||||||
errUnexpectedEOFStr = "unexpected_eof_str"
|
errUnexpectedEOFStr = "unexpected_eof_str"
|
||||||
errJSONSyntax = "json_syntax"
|
errJSONSyntax = "json_syntax"
|
||||||
errBlobTooBig = "blob_too_big"
|
errBlobTooBig = "blob_too_big"
|
||||||
|
errInvalidPeerJSON = "invalid_peer_json"
|
||||||
|
errInvalidPeerData = "invalid_peer_data"
|
||||||
|
errRequestTooLarge = "request_too_large"
|
||||||
errDeadlineExceeded = "deadline_exceeded"
|
errDeadlineExceeded = "deadline_exceeded"
|
||||||
errHashMismatch = "hash_mismatch"
|
errHashMismatch = "hash_mismatch"
|
||||||
|
errProtectedBlob = "protected_blob"
|
||||||
|
errInvalidBlobHash = "invalid_blob_hash"
|
||||||
errZeroByteBlob = "zero_byte_blob"
|
errZeroByteBlob = "zero_byte_blob"
|
||||||
errInvalidCharacter = "invalid_character"
|
errInvalidCharacter = "invalid_character"
|
||||||
errBlobNotFound = "blob_not_found"
|
errBlobNotFound = "blob_not_found"
|
||||||
|
@ -117,6 +123,11 @@ var (
|
||||||
Name: "http3_blob_download_total",
|
Name: "http3_blob_download_total",
|
||||||
Help: "Total number of blobs downloaded from reflector through QUIC protocol",
|
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{
|
CacheHitCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
|
@ -124,6 +135,18 @@ var (
|
||||||
Name: "hit_total",
|
Name: "hit_total",
|
||||||
Help: "Total number of blobs retrieved from the cache storage",
|
Help: "Total number of blobs retrieved from the cache storage",
|
||||||
}, []string{LabelCacheType, LabelComponent})
|
}, []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{
|
CacheMissCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
Subsystem: subsystemCache,
|
Subsystem: subsystemCache,
|
||||||
|
@ -136,7 +159,7 @@ var (
|
||||||
Name: "origin_requests_total",
|
Name: "origin_requests_total",
|
||||||
Help: "How many Get requests are in flight from the cache to the origin",
|
Help: "How many Get requests are in flight from the cache to the origin",
|
||||||
}, []string{LabelCacheType, LabelComponent})
|
}, []string{LabelCacheType, LabelComponent})
|
||||||
// during thundering-herd situations, the metric below should be a lot smaller than the metric above
|
//during thundering-herd situations, the metric below should be a lot smaller than the metric above
|
||||||
CacheWaitingRequestsCount = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
CacheWaitingRequestsCount = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
Subsystem: subsystemCache,
|
Subsystem: subsystemCache,
|
||||||
|
@ -181,11 +204,21 @@ var (
|
||||||
Name: "udp_in_bytes",
|
Name: "udp_in_bytes",
|
||||||
Help: "Total number of bytes downloaded through UDP",
|
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{
|
MtrOutBytesUdp = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
Name: "udp_out_bytes",
|
Name: "udp_out_bytes",
|
||||||
Help: "Total number of bytes streamed out through UDP",
|
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{
|
MtrInBytesReflector = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
Name: "reflector_in_bytes",
|
Name: "reflector_in_bytes",
|
||||||
|
@ -201,6 +234,21 @@ var (
|
||||||
Name: "s3_in_bytes",
|
Name: "s3_in_bytes",
|
||||||
Help: "Total number of incoming bytes (from S3-CF)",
|
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",
|
||||||
|
})
|
||||||
|
HttpBlobReqQueue = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: ns,
|
||||||
|
Name: "http_blob_request_queue_size",
|
||||||
|
Help: "Blob requests queue size of the HTTP protocol",
|
||||||
|
})
|
||||||
|
RoutinesQueue = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: ns,
|
||||||
|
Name: "routines",
|
||||||
|
Help: "routines running by type",
|
||||||
|
}, []string{"package", "kind"})
|
||||||
)
|
)
|
||||||
|
|
||||||
func CacheLabels(name, component string) prometheus.Labels {
|
func CacheLabels(name, component string) prometheus.Labels {
|
||||||
|
@ -253,10 +301,20 @@ func TrackError(direction string, e error) (shouldLog bool) { // shouldLog is a
|
||||||
} else if strings.Contains(err.Error(), "blob must be at most") {
|
} else if strings.Contains(err.Error(), "blob must be at most") {
|
||||||
//log.Warnln("blob must be at most X bytes is not the same as ErrBlobTooBig")
|
//log.Warnln("blob must be at most X bytes is not the same as ErrBlobTooBig")
|
||||||
errType = errBlobTooBig
|
errType = errBlobTooBig
|
||||||
|
} else if strings.Contains(err.Error(), "invalid json request") {
|
||||||
|
errType = errInvalidPeerJSON
|
||||||
|
} else if strings.Contains(err.Error(), "Invalid data") {
|
||||||
|
errType = errInvalidPeerData
|
||||||
|
} else if strings.Contains(err.Error(), "request is too large") {
|
||||||
|
errType = errRequestTooLarge
|
||||||
|
} else if strings.Contains(err.Error(), "Invalid blob hash length") {
|
||||||
|
errType = errInvalidBlobHash
|
||||||
} else if strings.Contains(err.Error(), "hash of received blob data does not match hash from send request") {
|
} else if strings.Contains(err.Error(), "hash of received blob data does not match hash from send request") {
|
||||||
errType = errHashMismatch
|
errType = errHashMismatch
|
||||||
} else if strings.Contains(err.Error(), "blob not found") {
|
} else if strings.Contains(err.Error(), "blob not found") {
|
||||||
errType = errBlobNotFound
|
errType = errBlobNotFound
|
||||||
|
} else if strings.Contains(err.Error(), "requested blob is protected") {
|
||||||
|
errType = errProtectedBlob
|
||||||
} else if strings.Contains(err.Error(), "0-byte blob received") {
|
} else if strings.Contains(err.Error(), "0-byte blob received") {
|
||||||
errType = errZeroByteBlob
|
errType = errZeroByteBlob
|
||||||
} else if strings.Contains(err.Error(), "PROTOCOL_VIOLATION: tried to retire connection") {
|
} else if strings.Contains(err.Error(), "PROTOCOL_VIOLATION: tried to retire connection") {
|
||||||
|
|
348
lite_db/db.go
Normal file
348
lite_db/db.go
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
package lite_db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
qt "github.com/lbryio/lbry.go/v2/extras/query"
|
||||||
|
|
||||||
|
"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/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SdBlob is a special blob that contains information on the rest of the blobs in the stream
|
||||||
|
type SdBlob struct {
|
||||||
|
StreamName string `json:"stream_name"`
|
||||||
|
Blobs []struct {
|
||||||
|
Length int `json:"length"`
|
||||||
|
BlobNum int `json:"blob_num"`
|
||||||
|
BlobHash string `json:"blob_hash,omitempty"`
|
||||||
|
IV string `json:"iv"`
|
||||||
|
} `json:"blobs"`
|
||||||
|
StreamType string `json:"stream_type"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
SuggestedFileName string `json:"suggested_file_name"`
|
||||||
|
StreamHash string `json:"stream_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQL implements the DB interface
|
||||||
|
type SQL struct {
|
||||||
|
conn *sql.DB
|
||||||
|
|
||||||
|
TrackAccessTime bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func logQuery(query string, args ...interface{}) {
|
||||||
|
s, err := qt.InterpolateParams(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
} else {
|
||||||
|
log.Debugln(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect will create a connection to the database
|
||||||
|
func (s *SQL) Connect(dsn string) error {
|
||||||
|
var err error
|
||||||
|
// interpolateParams is necessary. otherwise uploading a stream with thousands of blobs
|
||||||
|
// will hit MySQL's max_prepared_stmt_count limit because the prepared statements are all
|
||||||
|
// opened inside a transaction. closing them manually doesn't seem to help
|
||||||
|
dsn += "?parseTime=1&collation=utf8mb4_unicode_ci&interpolateParams=1"
|
||||||
|
s.conn, err = sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conn.SetMaxIdleConns(12)
|
||||||
|
|
||||||
|
return errors.Err(s.conn.Ping())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBlob adds a blob to the database.
|
||||||
|
func (s *SQL) AddBlob(hash string, length int) error {
|
||||||
|
if s.conn == nil {
|
||||||
|
return errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.insertBlob(hash, length)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQL) insertBlob(hash string, length int) (int64, error) {
|
||||||
|
if length <= 0 {
|
||||||
|
return 0, errors.Err("length must be positive")
|
||||||
|
}
|
||||||
|
const isStored = true
|
||||||
|
now := time.Now()
|
||||||
|
args := []interface{}{hash, isStored, length, now}
|
||||||
|
blobID, err := s.exec(
|
||||||
|
"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)",
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if blobID == 0 {
|
||||||
|
err = s.conn.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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blobID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBlob checks if the database contains the blob information.
|
||||||
|
func (s *SQL) HasBlob(hash string) (bool, error) {
|
||||||
|
exists, err := s.HasBlobs([]string{hash})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return exists[hash], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
return exists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQL) touch(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(query, args...)
|
||||||
|
log.Debugf("blobs access query touched %d blobs and took %s", len(blobIDs), time.Since(startTime))
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQL) hasBlobs(hashes []string) (map[string]bool, []uint64, error) {
|
||||||
|
if s.conn == nil {
|
||||||
|
return nil, nil, errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hash string
|
||||||
|
blobID 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
|
||||||
|
maxBatchSize := 10000
|
||||||
|
doneIndex := 0
|
||||||
|
|
||||||
|
for len(hashes) > doneIndex {
|
||||||
|
sliceEnd := doneIndex + maxBatchSize
|
||||||
|
if sliceEnd > len(hashes) {
|
||||||
|
sliceEnd = len(hashes)
|
||||||
|
}
|
||||||
|
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 hash, id, last_accessed_at
|
||||||
|
FROM blob_
|
||||||
|
WHERE is_stored = ? and hash IN (` + qt.Qs(len(batch)) + `)`
|
||||||
|
args := make([]interface{}, len(batch)+1)
|
||||||
|
args[0] = true
|
||||||
|
for i := range batch {
|
||||||
|
args[i+1] = batch[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
logQuery(query, args...)
|
||||||
|
|
||||||
|
err := func() error {
|
||||||
|
startTime := time.Now()
|
||||||
|
rows, err := s.conn.Query(query, args...)
|
||||||
|
log.Debugf("hashes query took %s", time.Since(startTime))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
defer closeRows(rows)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
err := rows.Scan(&hash, &blobID, &lastAccessedAt)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
exists[hash] = true
|
||||||
|
if s.TrackAccessTime && (!lastAccessedAt.Valid || lastAccessedAt.Time.Before(touchDeadline)) {
|
||||||
|
needsTouch = append(needsTouch, blobID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doneIndex += len(batch)
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists, needsTouch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete will remove the blob from the db
|
||||||
|
func (s *SQL) Delete(hash string) error {
|
||||||
|
_, err := s.exec("UPDATE blob_ set is_stored = ? WHERE hash = ?", 0, hash)
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSDBlob insert the SD blob and all the content blobs. The content blobs are marked as "not stored",
|
||||||
|
// but they are tracked so reflector knows what it is missing.
|
||||||
|
func (s *SQL) AddSDBlob(sdHash string, sdBlobLength int) error {
|
||||||
|
if s.conn == nil {
|
||||||
|
return errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.insertBlob(sdHash, sdBlobLength)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHashRange gets the smallest and biggest hashes in the db
|
||||||
|
func (s *SQL) GetLRUBlobs(maxBlobs int) ([]string, error) {
|
||||||
|
if s.conn == nil {
|
||||||
|
return nil, errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "SELECT hash from blob_ where is_stored = ? order by last_accessed_at limit ?"
|
||||||
|
const isStored = true
|
||||||
|
logQuery(query, isStored, maxBlobs)
|
||||||
|
rows, err := s.conn.Query(query, isStored, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQL) AllBlobs() ([]string, error) {
|
||||||
|
if s.conn == nil {
|
||||||
|
return nil, errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "SELECT hash from blob_ where is_stored = ?" //TODO: maybe sorting them makes more sense?
|
||||||
|
const isStored = true
|
||||||
|
logQuery(query, isStored)
|
||||||
|
rows, err := s.conn.Query(query, isStored)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Err(err)
|
||||||
|
}
|
||||||
|
defer closeRows(rows)
|
||||||
|
totalBlobs, err := s.BlobsCount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blobs := make([]string, 0, totalBlobs)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQL) BlobsCount() (int, error) {
|
||||||
|
if s.conn == nil {
|
||||||
|
return 0, errors.Err("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "SELECT count(id) from blob_ where is_stored = ?" //TODO: maybe sorting them makes more sense?
|
||||||
|
const isStored = true
|
||||||
|
logQuery(query, isStored)
|
||||||
|
var count int
|
||||||
|
err := s.conn.QueryRow(query, isStored).Scan(&count)
|
||||||
|
return count, errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeRows(rows *sql.Rows) {
|
||||||
|
if rows != nil {
|
||||||
|
err := rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error closing rows: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQL) exec(query string, args ...interface{}) (int64, error) {
|
||||||
|
logQuery(query, args...)
|
||||||
|
attempt, maxAttempts := 0, 3
|
||||||
|
Retry:
|
||||||
|
attempt++
|
||||||
|
result, err := s.conn.Exec(query, args...)
|
||||||
|
if isLockTimeoutError(err) {
|
||||||
|
if attempt <= maxAttempts {
|
||||||
|
//Error 1205: Lock wait timeout exceeded; try restarting transaction
|
||||||
|
goto Retry
|
||||||
|
}
|
||||||
|
err = errors.Prefix("Lock timeout for query "+query, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastID, err := result.LastInsertId()
|
||||||
|
return lastID, errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLockTimeoutError(err error) bool {
|
||||||
|
e, ok := err.(*mysql.MySQLError)
|
||||||
|
return ok && e != nil && e.Number == 1205
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SQL schema
|
||||||
|
|
||||||
|
in prod make sure you use latin1 or utf8 charset, NOT utf8mb4. that's a waste of space.
|
||||||
|
|
||||||
|
CREATE TABLE `blob_` (
|
||||||
|
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`hash` char(96) NOT NULL,
|
||||||
|
`is_stored` tinyint(1) NOT NULL DEFAULT '0',
|
||||||
|
`length` bigint unsigned DEFAULT NULL,
|
||||||
|
`last_accessed_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id` (`id`),
|
||||||
|
UNIQUE KEY `blob_hash_idx` (`hash`),
|
||||||
|
KEY `blob_last_accessed_idx` (`last_accessed_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=latin1
|
||||||
|
|
||||||
|
*/
|
38
meta/meta.go
38
meta/meta.go
|
@ -6,9 +6,36 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = ""
|
var (
|
||||||
var Time = ""
|
name = "prism-bin"
|
||||||
var BuildTime time.Time
|
version = "unknown"
|
||||||
|
commit = "unknown"
|
||||||
|
commitLong = "unknown"
|
||||||
|
branch = "unknown"
|
||||||
|
Time = "unknown"
|
||||||
|
BuildTime time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
// Name returns main application name
|
||||||
|
func Name() string {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns current application version
|
||||||
|
func Version() string {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullName returns current app version, commit and build time
|
||||||
|
func FullName() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`Name: %v
|
||||||
|
Version: %v
|
||||||
|
branch: %v
|
||||||
|
commit: %v
|
||||||
|
commit long: %v
|
||||||
|
build date: %v`, Name(), Version(), branch, commit, commitLong, BuildTime.String())
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if Time != "" {
|
if Time != "" {
|
||||||
|
@ -20,11 +47,6 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func VersionString() string {
|
func VersionString() string {
|
||||||
version := Version
|
|
||||||
if version == "" {
|
|
||||||
version = "<unset>"
|
|
||||||
}
|
|
||||||
|
|
||||||
var buildTime string
|
var buildTime string
|
||||||
if BuildTime.IsZero() {
|
if BuildTime.IsZero() {
|
||||||
buildTime = "<now>"
|
buildTime = "<now>"
|
||||||
|
|
|
@ -5,14 +5,14 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht"
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
|
||||||
"github.com/lbryio/reflector.go/cluster"
|
"github.com/lbryio/reflector.go/cluster"
|
||||||
"github.com/lbryio/reflector.go/db"
|
"github.com/lbryio/reflector.go/db"
|
||||||
"github.com/lbryio/reflector.go/peer"
|
|
||||||
"github.com/lbryio/reflector.go/reflector"
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
|
"github.com/lbryio/reflector.go/server/peer"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/dht"
|
||||||
|
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ func New(conf *Config) *Prism {
|
||||||
dht: d,
|
dht: d,
|
||||||
cluster: c,
|
cluster: c,
|
||||||
peer: peer.NewServer(conf.Blobs),
|
peer: peer.NewServer(conf.Blobs),
|
||||||
reflector: reflector.NewServer(conf.Blobs),
|
reflector: reflector.NewServer(conf.Blobs, conf.Blobs),
|
||||||
|
|
||||||
grp: stop.New(),
|
grp: stop.New(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAnnounceRange(t *testing.T) {
|
func TestAnnounceRange(t *testing.T) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package publish
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -21,27 +20,33 @@ import (
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var TODO = `
|
/* TODO:
|
||||||
import cert from wallet
|
import cert from wallet
|
||||||
get all utxos from chainquery
|
get all utxos from chainquery
|
||||||
create transaction
|
create transaction
|
||||||
sign it with the channel
|
sign it with the channel
|
||||||
track state of utxos across publishes from this channel so that we can just do one query to get utxos
|
track state of utxos across publishes from this channel so that we can just do one query to get utxos
|
||||||
prioritize only confirmed utxos
|
prioritize only confirmed utxos
|
||||||
|
|
||||||
Handling all the issues we handle currently with lbrynet:
|
Handling all the issues we handle currently with lbrynet:
|
||||||
"Couldn't find private key for id",
|
"Couldn't find private key for id",
|
||||||
"You already have a stream claim published under the name",
|
"You already have a stream claim published under the name",
|
||||||
"Cannot publish using channel",
|
"Cannot publish using channel",
|
||||||
"txn-mempool-conflict",
|
"txn-mempool-conflict",
|
||||||
"too-long-mempool-chain",
|
"too-long-mempool-chain",
|
||||||
"Missing inputs",
|
"Missing inputs",
|
||||||
"Not enough funds to cover this transaction",
|
"Not enough funds to cover this transaction",
|
||||||
|
*/
|
||||||
|
|
||||||
|
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) {
|
func Publish(client *lbrycrd.Client, path, name, address string, details Details, reflectorAddress string) (*wire.MsgTx, *chainhash.Hash, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
@ -69,11 +74,20 @@ func Publish(client *lbrycrd.Client, path, name, address string, details Details
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
claim, st, err := makeClaimAndStream(path, details)
|
st, stPB, err := makeStream(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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)
|
err = addClaimToTx(tx, claim, name, amount, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -101,7 +115,7 @@ func Publish(client *lbrycrd.Client, path, name, address string, details Details
|
||||||
return signedTx, txid, nil
|
return signedTx, txid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: lots of assumptions. hardcoded values need to be passed in or calculated
|
// TODO: lots of assumptions. hardcoded values need to be passed in or calculated
|
||||||
func baseTx(client *lbrycrd.Client, amount float64, changeAddress btcutil.Address) (*wire.MsgTx, error) {
|
func baseTx(client *lbrycrd.Client, amount float64, changeAddress btcutil.Address) (*wire.MsgTx, error) {
|
||||||
txFee := 0.0002 // TODO: estimate this better?
|
txFee := 0.0002 // TODO: estimate this better?
|
||||||
|
|
||||||
|
@ -203,50 +217,30 @@ func reflect(st stream.Stream, reflectorAddress string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Details struct {
|
func makeStream(path string) (stream.Stream, *pb.Stream, error) {
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Author string
|
|
||||||
Tags []string
|
|
||||||
ReleaseTime int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeClaimAndStream(path string, details Details) (*pb.Claim, stream.Stream, error) {
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Err(err)
|
return nil, nil, errors.Err(err)
|
||||||
}
|
}
|
||||||
data, err := ioutil.ReadAll(file)
|
defer func() { _ = file.Close() }()
|
||||||
if err != nil {
|
enc := stream.NewEncoder(file)
|
||||||
return nil, nil, errors.Err(err)
|
|
||||||
}
|
s, err := enc.Stream()
|
||||||
s, err := stream.New(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Err(err)
|
return nil, nil, errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make the claim
|
streamProto := &pb.Stream{
|
||||||
sdBlob := &stream.SDBlob{}
|
|
||||||
err = sdBlob.FromBlob(s[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filehash := sha3.Sum384(data)
|
|
||||||
|
|
||||||
streamPB := &pb.Stream{
|
|
||||||
Author: details.Author,
|
|
||||||
ReleaseTime: details.ReleaseTime,
|
|
||||||
Source: &pb.Source{
|
Source: &pb.Source{
|
||||||
SdHash: s[0].Hash(),
|
SdHash: enc.SDBlob().Hash(),
|
||||||
Name: filepath.Base(file.Name()),
|
Name: filepath.Base(file.Name()),
|
||||||
Size: uint64(len(data)),
|
Size: uint64(enc.SourceLen()),
|
||||||
Hash: filehash[:],
|
Hash: enc.SourceHash(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeType, category := guessMimeType(filepath.Ext(file.Name()))
|
mimeType, category := guessMimeType(filepath.Ext(file.Name()))
|
||||||
streamPB.Source.MediaType = mimeType
|
streamProto.Source.MediaType = mimeType
|
||||||
|
|
||||||
switch category {
|
switch category {
|
||||||
case "video":
|
case "video":
|
||||||
|
@ -254,20 +248,14 @@ func makeClaimAndStream(path string, details Details) (*pb.Claim, stream.Stream,
|
||||||
//if err != nil {
|
//if err != nil {
|
||||||
// return nil, nil, err
|
// return nil, nil, err
|
||||||
//}
|
//}
|
||||||
streamPB.Type = &pb.Stream_Video{}
|
streamProto.Type = &pb.Stream_Video{}
|
||||||
case "audio":
|
case "audio":
|
||||||
streamPB.Type = &pb.Stream_Audio{}
|
streamProto.Type = &pb.Stream_Audio{}
|
||||||
case "image":
|
case "image":
|
||||||
streamPB.Type = &pb.Stream_Image{}
|
streamProto.Type = &pb.Stream_Image{}
|
||||||
}
|
}
|
||||||
|
|
||||||
claim := &pb.Claim{
|
return s, streamProto, nil
|
||||||
Title: details.Title,
|
|
||||||
Description: details.Description,
|
|
||||||
Type: &pb.Claim_Stream{Stream: streamPB},
|
|
||||||
}
|
|
||||||
|
|
||||||
return claim, s, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClaimPayoutScript(name string, value []byte, address btcutil.Address) ([]byte, error) {
|
func getClaimPayoutScript(name string, value []byte, address btcutil.Address) ([]byte, error) {
|
||||||
|
|
102
readme.md
102
readme.md
|
@ -1,25 +1,110 @@
|
||||||
# Reflector
|
# Reflector
|
||||||
|
|
||||||
A reflector cluster to accept LBRY content for hosting en masse, rehost the content, and make money on data fees (TODO).
|
Reflector is a central piece of software that providers LBRY with the following features:
|
||||||
This code includes Go implementations of the LBRY peer protocol, reflector protocol, and DHT.
|
- Blobs reflection: when something is published, we capture the data and store it on our servers for quicker retrieval
|
||||||
|
- Blobs distribution: when a piece of content is requested and the LBRY network doesn't have it, reflector will retrieve it from its storage and distribute it
|
||||||
|
- Blobs caching: reflectors can be chained together in multiple regions or servers to form a chain of cached content. We call those "blobcaches". They are layered so that content distribution is favorable in all the regions we deploy it to
|
||||||
|
|
||||||
|
There are a few other features embedded in reflector.go including publishing streams from Go, downloading or upload blobs, resolving content and more unfinished tools.
|
||||||
|
|
||||||
|
This code includes a Go implementations of the LBRY peer protocol, reflector protocol, and DHT.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
coming soon
|
- Install mysql 8 (5.7 might work too)
|
||||||
|
- add a reflector user and database with password `reflector` with localhost access only
|
||||||
|
- Create the tables as described [here](https://github.com/lbryio/reflector.go/blob/master/db/db.go#L735) (the link might not update as the code does so just look for the schema in that file)
|
||||||
|
|
||||||
|
#### We do not support running reflector.go as a blob receiver, however if you want to run it as a private blobcache you may compile it yourself and run it as following:
|
||||||
|
```bash
|
||||||
|
./prism-bin reflector \
|
||||||
|
--conf="none" \
|
||||||
|
--disable-uploads=true \
|
||||||
|
--use-db=false \
|
||||||
|
--upstream-reflector="reflector.lbry.com" \
|
||||||
|
--upstream-protocol="http" \
|
||||||
|
--request-queue-size=200 \
|
||||||
|
--disk-cache="2GB:/path/to/your/storage/:localdb" \
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a systemd script if you want to run it automatically on startup or as a service.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
coming soon
|
Usage as reflector/blobcache:
|
||||||
|
```bash
|
||||||
|
Run reflector server
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
prism reflector [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--disable-blocklist Disable blocklist watching/updating
|
||||||
|
--disable-uploads Disable uploads to this reflector server
|
||||||
|
--disk-cache string Where to cache blobs on the file system. format is 'sizeGB:CACHE_PATH:cachemanager' (cachemanagers: localdb/lfuda/lru) (default "100GB:/tmp/downloaded_blobs:localdb")
|
||||||
|
-h, --help help for reflector
|
||||||
|
--http-peer-port int The port reflector will distribute content from over HTTP protocol (default 5569)
|
||||||
|
--http3-peer-port int The port reflector will distribute content from over HTTP3 protocol (default 5568)
|
||||||
|
--mem-cache int enable in-memory cache with a max size of this many blobs
|
||||||
|
--metrics-port int The port reflector will use for prometheus metrics (default 2112)
|
||||||
|
--optional-disk-cache string Optional secondary file system cache for blobs. format is 'sizeGB:CACHE_PATH:cachemanager' (cachemanagers: localdb/lfuda/lru) (this would get hit before the one specified in disk-cache)
|
||||||
|
--origin-endpoint string HTTP edge endpoint for standard HTTP retrieval
|
||||||
|
--origin-endpoint-fallback string HTTP edge endpoint for standard HTTP retrieval if first origin fails
|
||||||
|
--receiver-port int The port reflector will receive content from (default 5566)
|
||||||
|
--request-queue-size int How many concurrent requests from downstream should be handled at once (the rest will wait) (default 200)
|
||||||
|
--tcp-peer-port int The port reflector will distribute content from for the TCP (LBRY) protocol (default 5567)
|
||||||
|
--upstream-protocol string protocol used to fetch blobs from another upstream reflector server (tcp/http3/http) (default "http")
|
||||||
|
--upstream-reflector string host:port of a reflector server where blobs are fetched from
|
||||||
|
--use-db Whether to connect to the reflector db or not (default true)
|
||||||
|
|
||||||
|
Global Flags:
|
||||||
|
--conf string Path to config. Use 'none' to disable (default "config.json")
|
||||||
|
-v, --verbose strings Verbose logging for specific components
|
||||||
|
```
|
||||||
|
|
||||||
|
Other uses:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Prism is a single entry point application with multiple sub modules which can be leveraged individually or together
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
prism [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
check-integrity check blobs integrity for a given path
|
||||||
|
cluster Start(join) to or Start a new cluster
|
||||||
|
decode Decode a claim value
|
||||||
|
dht Run dht node
|
||||||
|
getstream Get a stream from a reflector server
|
||||||
|
help Help about any command
|
||||||
|
peer Run peer server
|
||||||
|
populate-db populate local database with blobs from a disk storage
|
||||||
|
publish Publish a file
|
||||||
|
reflector Run reflector server
|
||||||
|
resolve Resolve a URL
|
||||||
|
send Send a file to a reflector
|
||||||
|
sendblob Send a random blob to a reflector server
|
||||||
|
start Runs full prism application with cluster, dht, peer server, and reflector server.
|
||||||
|
test Test things
|
||||||
|
upload Upload blobs to S3
|
||||||
|
version Print the version
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--conf string Path to config. Use 'none' to disable (default "config.json")
|
||||||
|
-h, --help help for prism
|
||||||
|
-v, --verbose strings Verbose logging for specific components
|
||||||
|
```
|
||||||
## Running from Source
|
## Running from Source
|
||||||
|
|
||||||
This project requires [Go v1.11](https://golang.org/doc/install) or higher because it uses Go modules.
|
This project requires [Go v1.20](https://golang.org/doc/install).
|
||||||
|
|
||||||
|
On Ubuntu you can install it with `sudo snap install go --classic`
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone git@github.com:lbryio/reflector.go.git
|
git clone git@github.com:lbryio/reflector.go.git
|
||||||
cd reflector.go
|
cd reflector.go
|
||||||
make
|
make
|
||||||
./bin/prism-bin
|
./dist/linux_amd64/prism-bin
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
@ -33,8 +118,7 @@ This project is MIT licensed.
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
We take security seriously. Please contact security@lbry.com regarding any security issues.
|
We take security seriously. Please contact security@lbry.com regarding any security issues.
|
||||||
Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it.
|
Our PGP key is [here](https://lbry.com/faq/pgp-key) if you need it.
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
The primary contact for this project is [@Nikooo777](https://github.com/Nikooo777) (niko-at-lbry.com)
|
||||||
The primary contact for this project is [@lyoshenka](https://github.com/lyoshenka) (grin@lbry.com)
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
"github.com/lbryio/reflector.go/wallet"
|
"github.com/lbryio/reflector.go/wallet"
|
||||||
|
|
||||||
|
@ -109,8 +110,9 @@ func sdHashesForOutpoints(walletServers, outpoints []string, stopper stop.Chan)
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "sdhashesforoutput").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "sdhashesforoutput").Dec()
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
case <-stopper:
|
case <-stopper:
|
||||||
|
|
81
reflector/protected_content.go
Normal file
81
reflector/protected_content.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package reflector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluele/gcache"
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
const protectedListURL = "https://api.odysee.com/file/list_protected"
|
||||||
|
|
||||||
|
type ProtectedContent struct {
|
||||||
|
SDHash string `json:"sd_hash"`
|
||||||
|
ClaimID string `json:"claim_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectedCache = gcache.New(10).Expiration(2 * time.Minute).Build()
|
||||||
|
|
||||||
|
func GetProtectedContent() (interface{}, error) {
|
||||||
|
cachedVal, err := protectedCache.Get("protected")
|
||||||
|
if err == nil && cachedVal != nil {
|
||||||
|
return cachedVal.(map[string]bool), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
method := "GET"
|
||||||
|
var r struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Data []ProtectedContent `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest(method, protectedListURL, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Err(err)
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Err(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = res.Body.Close() }()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.Err("unexpected status code %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
if err = json.NewDecoder(res.Body).Decode(&r); err != nil {
|
||||||
|
return nil, errors.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Success {
|
||||||
|
return nil, errors.Prefix("file/list_protected API call", r.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
protectedMap := make(map[string]bool, len(r.Data))
|
||||||
|
for _, pc := range r.Data {
|
||||||
|
protectedMap[pc.SDHash] = true
|
||||||
|
}
|
||||||
|
err = protectedCache.Set("protected", protectedMap)
|
||||||
|
if err != nil {
|
||||||
|
return protectedMap, errors.Err(err)
|
||||||
|
}
|
||||||
|
return protectedMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sf = singleflight.Group{}
|
||||||
|
|
||||||
|
func IsProtected(sdHash string) bool {
|
||||||
|
val, err, _ := sf.Do("protected", GetProtectedContent)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cachedMap, ok := val.(map[string]bool)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedMap[sdHash]
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -40,16 +39,18 @@ type Server struct {
|
||||||
|
|
||||||
EnableBlocklist bool // if true, blocklist checking and blob deletion will be enabled
|
EnableBlocklist bool // if true, blocklist checking and blob deletion will be enabled
|
||||||
|
|
||||||
store store.BlobStore
|
underlyingStore store.BlobStore
|
||||||
grp *stop.Group
|
outerStore store.BlobStore
|
||||||
|
grp *stop.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer returns an initialized reflector server pointer.
|
// NewServer returns an initialized reflector server pointer.
|
||||||
func NewServer(store store.BlobStore) *Server {
|
func NewServer(underlying store.BlobStore, outer store.BlobStore) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
Timeout: DefaultTimeout,
|
Timeout: DefaultTimeout,
|
||||||
store: store,
|
underlyingStore: underlying,
|
||||||
grp: stop.New(),
|
outerStore: outer,
|
||||||
|
grp: stop.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,16 +61,17 @@ func (s *Server) Shutdown() {
|
||||||
log.Println("reflector server stopped")
|
log.Println("reflector server stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
//Start starts the server to handle connections.
|
// Start starts the server to handle connections.
|
||||||
func (s *Server) Start(address string) error {
|
func (s *Server) Start(address string) error {
|
||||||
l, err := net.Listen(network, address)
|
l, err := net.Listen(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
}
|
}
|
||||||
log.Println("reflector listening on " + address)
|
log.Println("reflector listening on " + address)
|
||||||
|
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "listener").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "listener").Dec()
|
||||||
<-s.grp.Ch()
|
<-s.grp.Ch()
|
||||||
err := l.Close()
|
err := l.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,15 +81,19 @@ func (s *Server) Start(address string) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "start").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "start").Dec()
|
||||||
s.listenAndServe(l)
|
s.listenAndServe(l)
|
||||||
s.grp.Done()
|
s.grp.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if s.EnableBlocklist {
|
if s.EnableBlocklist {
|
||||||
if b, ok := s.store.(store.Blocklister); ok {
|
if b, ok := s.underlyingStore.(store.Blocklister); ok {
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "enableblocklist").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "enableblocklist").Dec()
|
||||||
s.enableBlocklist(b)
|
s.enableBlocklist(b)
|
||||||
s.grp.Done()
|
s.grp.Done()
|
||||||
}()
|
}()
|
||||||
|
@ -110,7 +116,9 @@ func (s *Server) listenAndServe(listener net.Listener) {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
} else {
|
} else {
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "server-listenandserve").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "server-listenandserve").Dec()
|
||||||
s.handleConn(conn)
|
s.handleConn(conn)
|
||||||
s.grp.Done()
|
s.grp.Done()
|
||||||
}()
|
}()
|
||||||
|
@ -125,7 +133,9 @@ func (s *Server) handleConn(conn net.Conn) {
|
||||||
close(connNeedsClosing)
|
close(connNeedsClosing)
|
||||||
}()
|
}()
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "server-handleconn").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "server-handleconn").Dec()
|
||||||
defer s.grp.Done()
|
defer s.grp.Done()
|
||||||
select {
|
select {
|
||||||
case <-connNeedsClosing:
|
case <-connNeedsClosing:
|
||||||
|
@ -190,13 +200,13 @@ func (s *Server) receiveBlob(conn net.Conn) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var wantsBlob bool
|
var wantsBlob bool
|
||||||
if bl, ok := s.store.(store.Blocklister); ok {
|
if bl, ok := s.underlyingStore.(store.Blocklister); ok {
|
||||||
wantsBlob, err = bl.Wants(blobHash)
|
wantsBlob, err = bl.Wants(blobHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blobExists, err := s.store.Has(blobHash)
|
blobExists, err := s.underlyingStore.Has(blobHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -206,7 +216,7 @@ func (s *Server) receiveBlob(conn net.Conn) error {
|
||||||
var neededBlobs []string
|
var neededBlobs []string
|
||||||
|
|
||||||
if isSdBlob && !wantsBlob {
|
if isSdBlob && !wantsBlob {
|
||||||
if nbc, ok := s.store.(neededBlobChecker); ok {
|
if nbc, ok := s.underlyingStore.(neededBlobChecker); ok {
|
||||||
neededBlobs, err = nbc.MissingBlobsForKnownStream(blobHash)
|
neededBlobs, err = nbc.MissingBlobsForKnownStream(blobHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -249,9 +259,9 @@ func (s *Server) receiveBlob(conn net.Conn) error {
|
||||||
log.Debugln("Got blob " + blobHash[:8])
|
log.Debugln("Got blob " + blobHash[:8])
|
||||||
|
|
||||||
if isSdBlob {
|
if isSdBlob {
|
||||||
err = s.store.PutSD(blobHash, blob)
|
err = s.outerStore.PutSD(blobHash, blob)
|
||||||
} else {
|
} else {
|
||||||
err = s.store.Put(blobHash, blob)
|
err = s.outerStore.Put(blobHash, blob)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -356,7 +366,7 @@ func (s *Server) read(conn net.Conn, v interface{}) error {
|
||||||
dec := json.NewDecoder(conn)
|
dec := json.NewDecoder(conn)
|
||||||
err = dec.Decode(v)
|
err = dec.Decode(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data, _ := ioutil.ReadAll(dec.Buffered())
|
data, _ := io.ReadAll(dec.Buffered())
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
return errors.Err("%s. Data: %s", err.Error(), hex.EncodeToString(data))
|
return errors.Err("%s. Data: %s", err.Error(), hex.EncodeToString(data))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/dht/bits"
|
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/dht/bits"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/phayes/freeport"
|
"github.com/phayes/freeport"
|
||||||
)
|
)
|
||||||
|
@ -22,7 +23,7 @@ func startServerOnRandomPort(t *testing.T) (*Server, int) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := NewServer(store.NewMemStore())
|
srv := NewServer(store.NewMemStore(), store.NewMemStore())
|
||||||
err = srv.Start("127.0.0.1:" + strconv.Itoa(port))
|
err = srv.Start("127.0.0.1:" + strconv.Itoa(port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -119,7 +120,7 @@ func TestServer_Timeout(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := NewServer(store.NewMemStore())
|
srv := NewServer(store.NewMemStore(), store.NewMemStore())
|
||||||
srv.Timeout = testTimeout
|
srv.Timeout = testTimeout
|
||||||
err = srv.Start("127.0.0.1:" + strconv.Itoa(port))
|
err = srv.Start("127.0.0.1:" + strconv.Itoa(port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -190,7 +191,7 @@ func TestServer_PartialUpload(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := NewServer(st)
|
srv := NewServer(st, st)
|
||||||
err = srv.Start("127.0.0.1:" + strconv.Itoa(port))
|
err = srv.Start("127.0.0.1:" + strconv.Itoa(port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package reflector
|
package reflector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/db"
|
"github.com/lbryio/reflector.go/db"
|
||||||
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
@ -74,7 +74,7 @@ func (u *Uploader) Upload(dirOrFilePath string) error {
|
||||||
|
|
||||||
var exists map[string]bool
|
var exists map[string]bool
|
||||||
if !u.skipExistsCheck {
|
if !u.skipExistsCheck {
|
||||||
exists, err = u.db.HasBlobs(hashes)
|
exists, err = u.db.HasBlobs(hashes, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,9 @@ func (u *Uploader) Upload(dirOrFilePath string) error {
|
||||||
|
|
||||||
for i := 0; i < u.workers; i++ {
|
for i := 0; i < u.workers; i++ {
|
||||||
workerWG.Add(1)
|
workerWG.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "upload").Inc()
|
||||||
go func(i int) {
|
go func(i int) {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "upload").Dec()
|
||||||
defer workerWG.Done()
|
defer workerWG.Done()
|
||||||
defer func(i int) { log.Debugf("worker %d quitting", i) }(i)
|
defer func(i int) { log.Debugf("worker %d quitting", i) }(i)
|
||||||
u.worker(pathChan)
|
u.worker(pathChan)
|
||||||
|
@ -97,7 +99,9 @@ func (u *Uploader) Upload(dirOrFilePath string) error {
|
||||||
|
|
||||||
countWG := sync.WaitGroup{}
|
countWG := sync.WaitGroup{}
|
||||||
countWG.Add(1)
|
countWG.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("reflector", "uploader").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("reflector", "uploader").Dec()
|
||||||
defer countWG.Done()
|
defer countWG.Done()
|
||||||
u.counter()
|
u.counter()
|
||||||
}()
|
}()
|
||||||
|
@ -160,7 +164,7 @@ func (u *Uploader) uploadBlob(filepath string) (err error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
blob, err := ioutil.ReadFile(filepath)
|
blob, err := os.ReadFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Err(err)
|
return errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
26
scripts/lint.sh
Executable file
26
scripts/lint.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
err=0
|
||||||
|
trap 'err=1' ERR
|
||||||
|
# All the .go files, excluding auto generated folders
|
||||||
|
GO_FILES=$(find . -iname '*.go' -type f)
|
||||||
|
(
|
||||||
|
go install golang.org/x/tools/cmd/goimports@latest # Used in build script for generated files
|
||||||
|
# go install golang.org/x/lint/golint@latest # Linter
|
||||||
|
go install github.com/jgautheron/gocyclo@latest # Check against high complexity
|
||||||
|
go install github.com/mdempsky/unconvert@latest # Identifies unnecessary type conversions
|
||||||
|
go install github.com/kisielk/errcheck@latest # Checks for unhandled errors
|
||||||
|
go install github.com/opennota/check/cmd/varcheck@latest # Checks for unused vars
|
||||||
|
go install github.com/opennota/check/cmd/structcheck@latest # Checks for unused fields in structs
|
||||||
|
)
|
||||||
|
echo "Running varcheck..." && varcheck $(go list ./...)
|
||||||
|
echo "Running structcheck..." && structcheck $(go list ./...)
|
||||||
|
# go vet is the official Go static analyzer
|
||||||
|
echo "Running go vet..." && go vet $(go list ./...)
|
||||||
|
# checks for unhandled errors
|
||||||
|
echo "Running errcheck..." && errcheck $(go list ./...)
|
||||||
|
# check for unnecessary conversions - ignore autogen code
|
||||||
|
echo "Running unconvert..." && unconvert -v $(go list ./...)
|
||||||
|
echo "Running gocyclo..." && gocyclo -ignore "_test" -avg -over 28 $GO_FILES
|
||||||
|
#echo "Running golint..." && golint -set_exit_status $(go list ./...)
|
||||||
|
test $err = 0 # Return non-zero if any command failed
|
105
server/http/routes.go
Normal file
105
server/http/routes.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) getBlob(c *gin.Context) {
|
||||||
|
waiter := &sync.WaitGroup{}
|
||||||
|
waiter.Add(1)
|
||||||
|
enqueue(&blobRequest{c: c, finished: waiter})
|
||||||
|
waiter.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleGetBlob(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("Recovered from panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
start := time.Now()
|
||||||
|
hash := c.Query("hash")
|
||||||
|
edgeToken := c.Query("edge_token")
|
||||||
|
|
||||||
|
if reflector.IsProtected(hash) && edgeToken != s.edgeToken {
|
||||||
|
_ = c.Error(errors.Err("requested blob is protected"))
|
||||||
|
c.String(http.StatusForbidden, "requested blob is protected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.missesCache.Has(hash) {
|
||||||
|
serialized, err := shared.NewBlobTrace(time.Since(start), "http").Serialize()
|
||||||
|
c.Header("Via", serialized)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.Error(errors.Err(err))
|
||||||
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blob, trace, err := s.store.Get(hash)
|
||||||
|
if err != nil {
|
||||||
|
serialized, serializeErr := trace.Serialize()
|
||||||
|
if serializeErr != nil {
|
||||||
|
_ = c.Error(errors.Prefix(serializeErr.Error(), err))
|
||||||
|
c.String(http.StatusInternalServerError, errors.Prefix(serializeErr.Error(), err).Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("Via", serialized)
|
||||||
|
|
||||||
|
if errors.Is(err, store.ErrBlobNotFound) {
|
||||||
|
_ = s.missesCache.Set(hash, true)
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = c.Error(err)
|
||||||
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serialized, err := trace.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
_ = c.Error(err)
|
||||||
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
|
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.Error(err)
|
||||||
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) recoveryHandler(c *gin.Context, err interface{}) {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"title": "Error",
|
||||||
|
"err": err,
|
||||||
|
})
|
||||||
|
}
|
82
server/http/server.go
Normal file
82
server/http/server.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||||
|
|
||||||
|
"github.com/bluele/gcache"
|
||||||
|
nice "github.com/ekyoung/gin-nice-recovery"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
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
|
||||||
|
concurrentRequests int
|
||||||
|
missesCache gcache.Cache
|
||||||
|
edgeToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer returns an initialized Server pointer.
|
||||||
|
func NewServer(store store.BlobStore, requestQueueSize int, edgeToken string) *Server {
|
||||||
|
return &Server{
|
||||||
|
store: store,
|
||||||
|
grp: stop.New(),
|
||||||
|
concurrentRequests: requestQueueSize,
|
||||||
|
missesCache: gcache.New(2000).Expiration(5 * time.Minute).ARC().Build(),
|
||||||
|
edgeToken: edgeToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.New()
|
||||||
|
router.Use(gin.Logger())
|
||||||
|
// Install nice.Recovery, passing the handler to call after recovery
|
||||||
|
router.Use(nice.Recovery(s.recoveryHandler))
|
||||||
|
router.GET("/blob", s.getBlob)
|
||||||
|
router.HEAD("/blob", s.hasBlob)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
go s.listenForShutdown(srv)
|
||||||
|
go InitWorkers(s, s.concurrentRequests)
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
46
server/http/worker.go
Normal file
46
server/http/worker.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type blobRequest struct {
|
||||||
|
c *gin.Context
|
||||||
|
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("http", "worker").Inc()
|
||||||
|
go func(worker int) {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("http", "worker").Dec()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopper.Ch():
|
||||||
|
case r := <-getReqCh:
|
||||||
|
process(server, r)
|
||||||
|
metrics.HttpBlobReqQueue.Dec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueue(b *blobRequest) {
|
||||||
|
metrics.HttpBlobReqQueue.Inc()
|
||||||
|
getReqCh <- b
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(server *Server, r *blobRequest) {
|
||||||
|
server.HandleGetBlob(r.c)
|
||||||
|
r.finished.Done()
|
||||||
|
}
|
|
@ -9,12 +9,15 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/internal/metrics"
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
|
||||||
|
"github.com/quic-go/quic-go/http3"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is an instance of a client connected to a server.
|
// Client is an instance of a client connected to a server.
|
||||||
|
@ -35,7 +38,7 @@ func (c *Client) Close() error {
|
||||||
func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Stream, error) {
|
func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Stream, error) {
|
||||||
var sd stream.SDBlob
|
var sd stream.SDBlob
|
||||||
|
|
||||||
b, err := c.GetBlob(sdHash)
|
b, _, err := c.GetBlob(sdHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -49,10 +52,12 @@ func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Str
|
||||||
s[0] = b
|
s[0] = b
|
||||||
|
|
||||||
for i := 0; i < len(sd.BlobInfos)-1; i++ {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Debug(trace.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
|
@ -64,7 +69,7 @@ func (c *Client) HasBlob(hash string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Err(err)
|
return false, errors.Err(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode == http.StatusOK {
|
if resp.StatusCode == http.StatusOK {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -75,26 +80,35 @@ func (c *Client) HasBlob(hash string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlob gets a blob
|
// GetBlob gets a blob
|
||||||
func (c *Client) GetBlob(hash string) (stream.Blob, error) {
|
func (c *Client) GetBlob(hash string) (stream.Blob, shared.BlobTrace, error) {
|
||||||
resp, err := c.conn.Get(fmt.Sprintf("https://%s/get/%s", c.ServerAddr, hash))
|
start := time.Now()
|
||||||
|
resp, err := c.conn.Get(fmt.Sprintf("https://%s/get/%s?trace=true", c.ServerAddr, hash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Err(err)
|
return nil, shared.NewBlobTrace(time.Since(start), "http3"), errors.Err(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
fmt.Printf("%s blob not found %d\n", hash, resp.StatusCode)
|
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 {
|
} 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()
|
tmp := getBuffer()
|
||||||
defer putBuffer(tmp)
|
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)
|
written, err := io.Copy(tmp, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Err(err)
|
return nil, trace.Stack(time.Since(start), "http3"), errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blob := make([]byte, written)
|
blob := make([]byte, written)
|
||||||
|
@ -102,7 +116,7 @@ func (c *Client) GetBlob(hash string) (stream.Blob, error) {
|
||||||
|
|
||||||
metrics.MtrInBytesUdp.Add(float64(len(blob)))
|
metrics.MtrInBytesUdp.Add(float64(len(blob)))
|
||||||
|
|
||||||
return blob, nil
|
return blob, trace.Stack(time.Since(start), "http3"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buffer pool to reduce GC
|
// buffer pool to reduce GC
|
|
@ -10,31 +10,36 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/stop"
|
"github.com/lbryio/lbry.go/v2/extras/stop"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/lucas-clemente/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is an instance of a peer server that houses the listener and store.
|
// Server is an instance of a peer server that houses the listener and store.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
store store.BlobStore
|
store store.BlobStore
|
||||||
grp *stop.Group
|
grp *stop.Group
|
||||||
|
concurrentRequests int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer returns an initialized Server pointer.
|
// NewServer returns an initialized Server pointer.
|
||||||
func NewServer(store store.BlobStore) *Server {
|
func NewServer(store store.BlobStore, requestQueueSize int) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
store: store,
|
store: store,
|
||||||
grp: stop.New(),
|
grp: stop.New(),
|
||||||
|
concurrentRequests: requestQueueSize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,33 +68,21 @@ type availabilityResponse struct {
|
||||||
// Start starts the server listener to handle connections.
|
// Start starts the server listener to handle connections.
|
||||||
func (s *Server) Start(address string) error {
|
func (s *Server) Start(address string) error {
|
||||||
log.Println("HTTP3 peer listening on " + address)
|
log.Println("HTTP3 peer listening on " + address)
|
||||||
|
window500M := 500 * 1 << 20
|
||||||
|
|
||||||
quicConf := &quic.Config{
|
quicConf := &quic.Config{
|
||||||
HandshakeTimeout: 4 * time.Second,
|
MaxStreamReceiveWindow: uint64(window500M),
|
||||||
MaxIdleTimeout: 10 * time.Second,
|
MaxConnectionReceiveWindow: uint64(window500M),
|
||||||
|
EnableDatagrams: true,
|
||||||
|
HandshakeIdleTimeout: 4 * time.Second,
|
||||||
|
MaxIdleTimeout: 20 * time.Second,
|
||||||
}
|
}
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/get/{hash}", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/get/{hash}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
waiter := &sync.WaitGroup{}
|
||||||
requestedBlob := vars["hash"]
|
waiter.Add(1)
|
||||||
blob, err := s.store.Get(requestedBlob)
|
enqueue(&blobRequest{request: r, reply: w, finished: waiter})
|
||||||
if err != nil {
|
waiter.Wait()
|
||||||
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()
|
|
||||||
})
|
})
|
||||||
r.HandleFunc("/has/{hash}", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/has/{hash}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
|
@ -120,14 +113,12 @@ func (s *Server) Start(address string) error {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
server := http3.Server{
|
server := http3.Server{
|
||||||
Server: &http.Server{
|
Addr: address,
|
||||||
Handler: r,
|
Handler: r,
|
||||||
Addr: address,
|
TLSConfig: generateTLSConfig(),
|
||||||
TLSConfig: generateTLSConfig(),
|
|
||||||
},
|
|
||||||
QuicConfig: quicConf,
|
QuicConfig: quicConf,
|
||||||
}
|
}
|
||||||
|
go InitWorkers(s, s.concurrentRequests)
|
||||||
go s.listenForShutdown(&server)
|
go s.listenForShutdown(&server)
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -164,7 +155,7 @@ func generateTLSConfig() *tls.Config {
|
||||||
|
|
||||||
func (s *Server) listenAndServe(server *http3.Server) {
|
func (s *Server) listenAndServe(server *http3.Server) {
|
||||||
err := server.ListenAndServe()
|
err := server.ListenAndServe()
|
||||||
if err != nil && err.Error() != "server closed" {
|
if err != nil && err != quic.ErrServerClosed {
|
||||||
log.Errorln(errors.FullTrace(err))
|
log.Errorln(errors.FullTrace(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,3 +167,50 @@ func (s *Server) listenForShutdown(listener *http3.Server) {
|
||||||
log.Error("error closing listener for peer server - ", err)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reflector.IsProtected(requestedBlob) {
|
||||||
|
http.Error(w, "requested blob is protected", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
|
@ -4,18 +4,25 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
"github.com/quic-go/quic-go"
|
||||||
|
"github.com/quic-go/quic-go/http3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store is a blob store that gets blobs from a peer.
|
// Store is a blob store that gets blobs from a peer.
|
||||||
// It satisfies the store.BlobStore interface but cannot put or delete blobs.
|
// It satisfies the store.BlobStore interface but cannot put or delete blobs.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
opts StoreOpts
|
opts StoreOpts
|
||||||
|
NotFoundCache *sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreOpts allows to set options for a new Store.
|
// StoreOpts allows to set options for a new Store.
|
||||||
|
@ -26,13 +33,17 @@ type StoreOpts struct {
|
||||||
|
|
||||||
// NewStore makes a new peer store.
|
// NewStore makes a new peer store.
|
||||||
func NewStore(opts StoreOpts) *Store {
|
func NewStore(opts StoreOpts) *Store {
|
||||||
return &Store{opts: opts}
|
return &Store{opts: opts, NotFoundCache: &sync.Map{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Store) getClient() (*Client, error) {
|
func (p *Store) getClient() (*Client, error) {
|
||||||
var qconf quic.Config
|
var qconf quic.Config
|
||||||
qconf.HandshakeTimeout = 4 * time.Second
|
window500M := 500 * 1 << 20
|
||||||
qconf.MaxIdleTimeout = 10 * time.Second
|
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()
|
pool, err := x509.SystemCertPool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -63,31 +74,44 @@ func (p *Store) Has(hash string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer func() { _ = c.Close() }()
|
||||||
return c.HasBlob(hash)
|
return c.HasBlob(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get downloads the blob from the peer
|
// 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) {
|
||||||
c, err := p.getClient()
|
start := time.Now()
|
||||||
if err != nil {
|
if lastChecked, ok := p.NotFoundCache.Load(hash); ok {
|
||||||
return nil, err
|
if lastChecked.(time.Time).After(time.Now().Add(-5 * time.Minute)) {
|
||||||
|
return nil, shared.NewBlobTrace(time.Since(start), p.Name()+"-notfoundcache"), store.ErrBlobNotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defer c.Close()
|
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, shared.NewBlobTrace(time.Since(start), p.Name()), err
|
||||||
|
}
|
||||||
|
defer func() { _ = c.Close() }()
|
||||||
return c.GetBlob(hash)
|
return c.GetBlob(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put is not supported
|
// Put is not supported
|
||||||
func (p *Store) Put(hash string, blob stream.Blob) error {
|
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
|
// PutSD is not supported
|
||||||
func (p *Store) PutSD(hash string, blob stream.Blob) error {
|
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
|
// Delete is not supported
|
||||||
func (p *Store) Delete(hash string) error {
|
func (p *Store) Delete(hash string) error {
|
||||||
panic("http3Store cannot put or delete blobs")
|
return errors.Err(shared.ErrNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown is not supported
|
||||||
|
func (p *Store) Shutdown() {
|
||||||
}
|
}
|
46
server/http3/worker.go
Normal file
46
server/http3/worker.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueue(b *blobRequest) {
|
||||||
|
metrics.Http3BlobReqQueue.Inc()
|
||||||
|
getReqCh <- b
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(server *Server, r *blobRequest) {
|
||||||
|
server.HandleGetBlob(r.reply, r.request)
|
||||||
|
r.finished.Done()
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
@ -17,9 +18,6 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrBlobExists is a default error for when a blob already exists on the reflector server.
|
|
||||||
var ErrBlobExists = errors.Base("blob exists on server")
|
|
||||||
|
|
||||||
// Client is an instance of a client connected to a server.
|
// Client is an instance of a client connected to a server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
@ -57,10 +55,11 @@ func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Str
|
||||||
|
|
||||||
var sd stream.SDBlob
|
var sd stream.SDBlob
|
||||||
|
|
||||||
b, err := c.GetBlob(sdHash)
|
b, trace, err := c.GetBlob(sdHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Debug(trace.String())
|
||||||
|
|
||||||
err = sd.FromBlob(b)
|
err = sd.FromBlob(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -71,10 +70,11 @@ func (c *Client) GetStream(sdHash string, blobCache store.BlobStore) (stream.Str
|
||||||
s[0] = b
|
s[0] = b
|
||||||
|
|
||||||
for i := 0; i < len(sd.BlobInfos)-1; i++ {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Debug(trace.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
|
@ -114,47 +114,52 @@ func (c *Client) HasBlob(hash string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlob gets a blob
|
// 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 {
|
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{
|
sendRequest, err := json.Marshal(blobRequest{
|
||||||
RequestedBlob: hash,
|
RequestedBlob: hash,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), "tcp"), err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.write(sendRequest)
|
err = c.write(sendRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), "tcp"), err
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp blobResponse
|
var resp blobResponse
|
||||||
err = c.read(&resp)
|
err = c.read(&resp)
|
||||||
if err != nil {
|
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 != "" {
|
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 {
|
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 {
|
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())
|
log.Debugf("receiving blob %s from %s", hash[:8], c.conn.RemoteAddr())
|
||||||
|
|
||||||
blob, err := c.readRawBlob(resp.IncomingBlob.Length)
|
blob, err := c.readRawBlob(resp.IncomingBlob.Length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, (*resp.RequestTrace).Stack(time.Since(start), "tcp"), err
|
||||||
}
|
}
|
||||||
metrics.MtrInBytesTcp.Add(float64(len(blob)))
|
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 {
|
func (c *Client) read(v interface{}) error {
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
"github.com/lbryio/reflector.go/reflector"
|
"github.com/lbryio/reflector.go/reflector"
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"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))
|
log.Error(errors.Prefix("accepting conn", err))
|
||||||
} else {
|
} else {
|
||||||
s.grp.Add(1)
|
s.grp.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("peer", "server-handleconn").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer metrics.RoutinesQueue.WithLabelValues("peer", "server-handleconn").Dec()
|
||||||
s.handleConnection(conn)
|
s.handleConnection(conn)
|
||||||
s.grp.Done()
|
s.grp.Done()
|
||||||
}()
|
}()
|
||||||
|
@ -224,35 +227,40 @@ func (s *Server) handleCompositeRequest(data []byte) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var je *json.SyntaxError
|
var je *json.SyntaxError
|
||||||
if ee.As(err, &je) {
|
if ee.As(err, &je) {
|
||||||
return nil, errors.Err("invalid json at offset %d in data %s", je.Offset, hex.EncodeToString(data))
|
return nil, errors.Err("invalid json request: offset %d in data %s", je.Offset, hex.EncodeToString(data))
|
||||||
}
|
}
|
||||||
return nil, errors.Err(err)
|
return nil, errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := compositeResponse{
|
response := compositeResponse{
|
||||||
LbrycrdAddress: LbrycrdAddress,
|
LbrycrdAddress: LbrycrdAddress,
|
||||||
|
AvailableBlobs: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(request.RequestedBlobs) > 0 {
|
if len(request.RequestedBlobs) > 0 {
|
||||||
var availableBlobs []string
|
|
||||||
for _, blobHash := range request.RequestedBlobs {
|
for _, blobHash := range request.RequestedBlobs {
|
||||||
|
if reflector.IsProtected(blobHash) {
|
||||||
|
return nil, errors.Err("requested blob is protected")
|
||||||
|
}
|
||||||
exists, err := s.store.Has(blobHash)
|
exists, err := s.store.Has(blobHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
availableBlobs = append(availableBlobs, blobHash)
|
response.AvailableBlobs = append(response.AvailableBlobs, blobHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.AvailableBlobs = availableBlobs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response.BlobDataPaymentRate = paymentRateAccepted
|
if request.BlobDataPaymentRate != nil {
|
||||||
if request.BlobDataPaymentRate < 0 {
|
response.BlobDataPaymentRate = paymentRateAccepted
|
||||||
response.BlobDataPaymentRate = paymentRateTooLow
|
if *request.BlobDataPaymentRate < 0 {
|
||||||
|
response.BlobDataPaymentRate = paymentRateTooLow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var blob []byte
|
var blob []byte
|
||||||
|
var trace shared.BlobTrace
|
||||||
if request.RequestedBlob != "" {
|
if request.RequestedBlob != "" {
|
||||||
if len(request.RequestedBlob) != stream.BlobHashHexLength {
|
if len(request.RequestedBlob) != stream.BlobHashHexLength {
|
||||||
return nil, errors.Err("Invalid blob hash length")
|
return nil, errors.Err("Invalid blob hash length")
|
||||||
|
@ -260,16 +268,17 @@ func (s *Server) handleCompositeRequest(data []byte) ([]byte, error) {
|
||||||
|
|
||||||
log.Debugln("Sending blob " + request.RequestedBlob[:8])
|
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) {
|
if errors.Is(err, store.ErrBlobNotFound) {
|
||||||
response.IncomingBlob = incomingBlob{
|
response.IncomingBlob = &incomingBlob{
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
response.IncomingBlob = incomingBlob{
|
response.IncomingBlob = &incomingBlob{
|
||||||
BlobHash: reflector.BlobHash(blob),
|
BlobHash: request.RequestedBlob,
|
||||||
Length: len(blob),
|
Length: len(blob),
|
||||||
}
|
}
|
||||||
metrics.MtrOutBytesTcp.Add(float64(len(blob)))
|
metrics.MtrOutBytesTcp.Add(float64(len(blob)))
|
||||||
|
@ -297,7 +306,15 @@ func (s *Server) logError(e error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func readNextMessage(buf *bufio.Reader) ([]byte, error) {
|
func readNextMessage(buf *bufio.Reader) ([]byte, error) {
|
||||||
msg := make([]byte, 0)
|
first_byte, err := buf.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if first_byte != '{' {
|
||||||
|
// every request starts with '{'. Checking here disconnects earlier, so we don't wait until timeout
|
||||||
|
return nil, errInvalidData
|
||||||
|
}
|
||||||
|
msg := []byte("{")
|
||||||
eof := false
|
eof := false
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -318,6 +335,8 @@ func readNextMessage(buf *bufio.Reader) ([]byte, error) {
|
||||||
|
|
||||||
if len(msg) > maxRequestSize {
|
if len(msg) > maxRequestSize {
|
||||||
return msg, errRequestTooLarge
|
return msg, errRequestTooLarge
|
||||||
|
} else if len(msg) > 0 && msg[0] != '{' {
|
||||||
|
return msg, errInvalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
// yes, this is how the peer protocol knows when the request finishes
|
// yes, this is how the peer protocol knows when the request finishes
|
||||||
|
@ -352,6 +371,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var errRequestTooLarge = errors.Base("request is too large")
|
var errRequestTooLarge = errors.Base("request is too large")
|
||||||
|
var errInvalidData = errors.Base("Invalid data")
|
||||||
|
|
||||||
type availabilityRequest struct {
|
type availabilityRequest struct {
|
||||||
LbrycrdAddress bool `json:"lbrycrd_address"`
|
LbrycrdAddress bool `json:"lbrycrd_address"`
|
||||||
|
@ -382,18 +402,19 @@ type incomingBlob struct {
|
||||||
}
|
}
|
||||||
type blobResponse struct {
|
type blobResponse struct {
|
||||||
IncomingBlob incomingBlob `json:"incoming_blob"`
|
IncomingBlob incomingBlob `json:"incoming_blob"`
|
||||||
|
RequestTrace *shared.BlobTrace
|
||||||
}
|
}
|
||||||
|
|
||||||
type compositeRequest struct {
|
type compositeRequest struct {
|
||||||
LbrycrdAddress bool `json:"lbrycrd_address"`
|
LbrycrdAddress bool `json:"lbrycrd_address"`
|
||||||
RequestedBlobs []string `json:"requested_blobs"`
|
RequestedBlobs []string `json:"requested_blobs"`
|
||||||
BlobDataPaymentRate float64 `json:"blob_data_payment_rate"`
|
BlobDataPaymentRate *float64 `json:"blob_data_payment_rate"`
|
||||||
RequestedBlob string `json:"requested_blob"`
|
RequestedBlob string `json:"requested_blob"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type compositeResponse struct {
|
type compositeResponse struct {
|
||||||
LbrycrdAddress string `json:"lbrycrd_address,omitempty"`
|
LbrycrdAddress string `json:"lbrycrd_address,omitempty"`
|
||||||
AvailableBlobs []string `json:"available_blobs,omitempty"`
|
AvailableBlobs []string `json:"available_blobs"`
|
||||||
BlobDataPaymentRate string `json:"blob_data_payment_rate,omitempty"`
|
BlobDataPaymentRate string `json:"blob_data_payment_rate,omitempty"`
|
||||||
IncomingBlob incomingBlob `json:"incoming_blob,omitempty"`
|
IncomingBlob *incomingBlob `json:"incoming_blob,omitempty"`
|
||||||
}
|
}
|
|
@ -2,7 +2,10 @@ package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/store"
|
"github.com/lbryio/reflector.go/store"
|
||||||
)
|
)
|
||||||
|
@ -75,3 +78,62 @@ func TestAvailabilityRequest_WithBlobs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestFromConnection(t *testing.T) {
|
||||||
|
s := getServer(t, true)
|
||||||
|
err := s.Start("127.0.0.1:50505")
|
||||||
|
defer s.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error starting server", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range availabilityRequests {
|
||||||
|
conn, err := net.Dial("tcp", "127.0.0.1:50505")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error opening connection", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
response := make([]byte, 8192)
|
||||||
|
_, err = conn.Write(p.request)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error writing", err)
|
||||||
|
}
|
||||||
|
_, err = conn.Read(response)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error reading", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(response[:len(p.response)], p.response) {
|
||||||
|
t.Errorf("Response did not match expected response.\nExpected: %s\nGot: %s", string(p.response), string(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidData(t *testing.T) {
|
||||||
|
s := getServer(t, true)
|
||||||
|
err := s.Start("127.0.0.1:50503")
|
||||||
|
defer s.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error starting server", err)
|
||||||
|
}
|
||||||
|
conn, err := net.Dial("tcp", "127.0.0.1:50503")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error opening connection", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
response := make([]byte, 8192)
|
||||||
|
_, err = conn.Write([]byte("hello dear server, I would like blobs. Please"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error writing", err)
|
||||||
|
}
|
||||||
|
err = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error setting read deadline", err)
|
||||||
|
}
|
||||||
|
_, err = conn.Read(response)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Error("error reading", err)
|
||||||
|
}
|
||||||
|
println(response)
|
||||||
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
)
|
)
|
||||||
|
@ -38,31 +42,41 @@ func (p *Store) Has(hash string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer func() { _ = c.Close() }()
|
||||||
return c.HasBlob(hash)
|
return c.HasBlob(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get downloads the blob from the peer
|
// 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()
|
c, err := p.getClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), p.Name()), err
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer func() { _ = 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
|
// Put is not supported
|
||||||
func (p *Store) Put(hash string, blob stream.Blob) error {
|
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
|
// PutSD is not supported
|
||||||
func (p *Store) PutSD(hash string, blob stream.Blob) error {
|
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
|
// Delete is not supported
|
||||||
func (p *Store) Delete(hash string) error {
|
func (p *Store) Delete(hash string) error {
|
||||||
panic("PeerStore cannot put or delete blobs")
|
return errors.Err(shared.ErrNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown is not supported
|
||||||
|
func (p *Store) Shutdown() {
|
||||||
}
|
}
|
6
shared/errors.go
Normal file
6
shared/errors.go
Normal file
|
@ -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")
|
82
shared/shared.go
Normal file
82
shared/shared.go
Normal file
|
@ -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
|
||||||
|
}
|
36
shared/shared_test.go
Normal file
36
shared/shared_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/util"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlobTrace_Serialize(t *testing.T) {
|
||||||
|
hostName = util.PtrToString("test_machine")
|
||||||
|
stack := NewBlobTrace(10*time.Second, "test")
|
||||||
|
stack.Stack(20*time.Second, "test2")
|
||||||
|
stack.Stack(30*time.Second, "test3")
|
||||||
|
serialized, err := stack.Serialize()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Log(serialized)
|
||||||
|
expected := "{\"stacks\":[{\"timing\":10000000000,\"origin_name\":\"test\",\"host_name\":\"test_machine\"},{\"timing\":20000000000,\"origin_name\":\"test2\",\"host_name\":\"test_machine\"},{\"timing\":30000000000,\"origin_name\":\"test3\",\"host_name\":\"test_machine\"}]}"
|
||||||
|
assert.Equal(t, expected, serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlobTrace_Deserialize(t *testing.T) {
|
||||||
|
hostName = util.PtrToString("test_machine")
|
||||||
|
serialized := "{\"stacks\":[{\"timing\":10000000000,\"origin_name\":\"test\"},{\"timing\":20000000000,\"origin_name\":\"test2\"},{\"timing\":30000000000,\"origin_name\":\"test3\"}]}"
|
||||||
|
stack, err := Deserialize(serialized)
|
||||||
|
assert.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")
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package store
|
package store
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
return time.Unix(ts.Sec, ts.Nsec)
|
||||||
}
|
}
|
||||||
|
|
||||||
func atime(fi os.FileInfo) time.Time {
|
func atime(fi os.FileInfo) time.Time {
|
||||||
|
|
|
@ -3,10 +3,13 @@ package store
|
||||||
import (
|
import (
|
||||||
"time"
|
"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/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CachingStore combines two stores, typically a local and a remote store, to improve performance.
|
// CachingStore combines two stores, typically a local and a remote store, to improve performance.
|
||||||
|
@ -22,7 +25,7 @@ func NewCachingStore(component string, origin, cache BlobStore) *CachingStore {
|
||||||
return &CachingStore{
|
return &CachingStore{
|
||||||
component: component,
|
component: component,
|
||||||
origin: WithSingleFlight(component, origin),
|
origin: WithSingleFlight(component, origin),
|
||||||
cache: cache,
|
cache: WithSingleFlight(component, cache),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,9 +45,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
|
// 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.
|
// 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()
|
start := time.Now()
|
||||||
blob, err := c.cache.Get(hash)
|
blob, trace, err := c.cache.Get(hash)
|
||||||
if err == nil || !errors.Is(err, ErrBlobNotFound) {
|
if err == nil || !errors.Is(err, ErrBlobNotFound) {
|
||||||
metrics.CacheHitCount.With(metrics.CacheLabels(c.cache.Name(), c.component)).Inc()
|
metrics.CacheHitCount.With(metrics.CacheLabels(c.cache.Name(), c.component)).Inc()
|
||||||
rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds()
|
rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds()
|
||||||
|
@ -53,18 +56,21 @@ func (c *CachingStore) Get(hash string) (stream.Blob, error) {
|
||||||
metrics.LabelComponent: c.component,
|
metrics.LabelComponent: c.component,
|
||||||
metrics.LabelSource: "cache",
|
metrics.LabelSource: "cache",
|
||||||
}).Set(rate)
|
}).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()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, trace.Stack(time.Since(start), c.Name()), err
|
||||||
}
|
}
|
||||||
|
// do not do this async unless you're prepared to deal with mayhem
|
||||||
err = c.cache.Put(hash, blob)
|
err = c.cache.Put(hash, blob)
|
||||||
return blob, err
|
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
|
// Put stores the blob in the origin and the cache
|
||||||
|
@ -93,3 +99,9 @@ func (c *CachingStore) Delete(hash string) error {
|
||||||
}
|
}
|
||||||
return c.cache.Delete(hash)
|
return c.cache.Delete(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (c *CachingStore) Shutdown() {
|
||||||
|
c.origin.Shutdown()
|
||||||
|
c.cache.Shutdown()
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,13 +53,14 @@ func TestCachingStore_CacheMiss(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.Get(hash)
|
res, stack, err := s.Get(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(b, res) {
|
if !bytes.Equal(b, res) {
|
||||||
t.Errorf("expected Get() to return %s, got %s", string(b), string(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)
|
has, err := cache.Has(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -66,14 +69,16 @@ func TestCachingStore_CacheMiss(t *testing.T) {
|
||||||
if !has {
|
if !has {
|
||||||
t.Errorf("Get() did not copy blob to cache")
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(b, res) {
|
if !bytes.Equal(b, res) {
|
||||||
t.Errorf("expected cached Get() to return %s, got %s", string(b), string(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) {
|
func TestCachingStore_ThunderingHerd(t *testing.T) {
|
||||||
|
@ -92,7 +97,7 @@ func TestCachingStore_ThunderingHerd(t *testing.T) {
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
getNoErr := func() {
|
getNoErr := func() {
|
||||||
res, err := s.Get(hash)
|
res, _, err := s.Get(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -148,7 +153,7 @@ func (s *SlowBlobStore) Has(hash string) (bool, error) {
|
||||||
return s.mem.Has(hash)
|
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)
|
time.Sleep(s.delay)
|
||||||
return s.mem.Get(hash)
|
return s.mem.Get(hash)
|
||||||
}
|
}
|
||||||
|
@ -167,3 +172,7 @@ func (s *SlowBlobStore) Delete(hash string) error {
|
||||||
time.Sleep(s.delay)
|
time.Sleep(s.delay)
|
||||||
return s.mem.Delete(hash)
|
return s.mem.Delete(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SlowBlobStore) Shutdown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
"github.com/lbryio/reflector.go/meta"
|
"github.com/lbryio/reflector.go/meta"
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
@ -36,8 +36,7 @@ func (c *CloudFrontROStore) Has(hash string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer body.Close()
|
defer func() { _ = body.Close() }()
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case http.StatusNotFound, http.StatusForbidden:
|
case http.StatusNotFound, http.StatusForbidden:
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -49,30 +48,30 @@ func (c *CloudFrontROStore) Has(hash string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the blob from Cloudfront.
|
// 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])
|
log.Debugf("Getting %s from S3", hash[:8])
|
||||||
|
start := time.Now()
|
||||||
defer func(t time.Time) {
|
defer func(t time.Time) {
|
||||||
log.Debugf("Getting %s from S3 took %s", hash[:8], time.Since(t).String())
|
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)
|
status, body, err := c.cfRequest(http.MethodGet, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), c.Name()), err
|
||||||
}
|
}
|
||||||
defer body.Close()
|
defer func() { _ = body.Close() }()
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case http.StatusNotFound, http.StatusForbidden:
|
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:
|
case http.StatusOK:
|
||||||
b, err := ioutil.ReadAll(body)
|
b, err := io.ReadAll(body)
|
||||||
if err != nil {
|
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)))
|
metrics.MtrInBytesS3.Add(float64(len(b)))
|
||||||
return b, nil
|
return b, shared.NewBlobTrace(time.Since(start), c.Name()), nil
|
||||||
default:
|
default:
|
||||||
return nil, errors.Err("unexpected status %d", status)
|
return nil, shared.NewBlobTrace(time.Since(start), c.Name()), errors.Err("unexpected status %d", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +81,7 @@ func (c *CloudFrontROStore) cfRequest(method, hash string) (int, io.ReadCloser,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, errors.Err(err)
|
return 0, nil, errors.Err(err)
|
||||||
}
|
}
|
||||||
req.Header.Add("User-Agent", "reflector.go/"+meta.Version)
|
req.Header.Add("User-Agent", "reflector.go/"+meta.Version())
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -93,13 +92,17 @@ func (c *CloudFrontROStore) cfRequest(method, hash string) (int, io.ReadCloser,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CloudFrontROStore) Put(_ string, _ stream.Blob) error {
|
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 {
|
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 {
|
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() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 {
|
type CloudFrontRWStore struct {
|
||||||
cf *CloudFrontROStore
|
cf *ITTTStore
|
||||||
s3 *S3Store
|
s3 *S3Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCloudFrontRWStore returns an initialized CloudFrontRWStore store pointer.
|
// NewCloudFrontRWStore returns an initialized CloudFrontRWStore store pointer.
|
||||||
// NOTE: It panics if either argument is nil.
|
// 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 {
|
if cf == nil || s3 == nil {
|
||||||
panic("both stores must be set")
|
panic("both stores must be set")
|
||||||
}
|
}
|
||||||
|
@ -30,8 +34,10 @@ func (c *CloudFrontRWStore) Has(hash string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the blob from Cloudfront.
|
// Get gets the blob from Cloudfront.
|
||||||
func (c *CloudFrontRWStore) Get(hash string) (stream.Blob, error) {
|
func (c *CloudFrontRWStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) {
|
||||||
return c.cf.Get(hash)
|
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
|
// Put stores the blob on S3
|
||||||
|
@ -48,3 +54,9 @@ func (c *CloudFrontRWStore) PutSD(hash string, blob stream.Blob) error {
|
||||||
func (c *CloudFrontRWStore) Delete(hash string) error {
|
func (c *CloudFrontRWStore) Delete(hash string) error {
|
||||||
return c.s3.Delete(hash)
|
return c.s3.Delete(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (c *CloudFrontRWStore) Shutdown() {
|
||||||
|
c.s3.Shutdown()
|
||||||
|
c.cf.Shutdown()
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@ package store
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/db"
|
"github.com/lbryio/reflector.go/db"
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
@ -14,15 +16,16 @@ import (
|
||||||
|
|
||||||
// DBBackedStore is a store that's backed by a DB. The DB contains data about what's in the store.
|
// DBBackedStore is a store that's backed by a DB. The DB contains data about what's in the store.
|
||||||
type DBBackedStore struct {
|
type DBBackedStore struct {
|
||||||
blobs BlobStore
|
blobs BlobStore
|
||||||
db *db.SQL
|
db *db.SQL
|
||||||
blockedMu sync.RWMutex
|
blockedMu sync.RWMutex
|
||||||
blocked map[string]bool
|
blocked map[string]bool
|
||||||
|
deleteOnMiss bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDBBackedStore returns an initialized store pointer.
|
// NewDBBackedStore returns an initialized store pointer.
|
||||||
func NewDBBackedStore(blobs BlobStore, db *db.SQL) *DBBackedStore {
|
func NewDBBackedStore(blobs BlobStore, db *db.SQL, deleteOnMiss bool) *DBBackedStore {
|
||||||
return &DBBackedStore{blobs: blobs, db: db}
|
return &DBBackedStore{blobs: blobs, db: db, deleteOnMiss: deleteOnMiss}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameDBBacked = "db-backed"
|
const nameDBBacked = "db-backed"
|
||||||
|
@ -32,20 +35,29 @@ func (d *DBBackedStore) Name() string { return nameDBBacked }
|
||||||
|
|
||||||
// Has returns true if the blob is in the store
|
// Has returns true if the blob is in the store
|
||||||
func (d *DBBackedStore) Has(hash string) (bool, error) {
|
func (d *DBBackedStore) Has(hash string) (bool, error) {
|
||||||
return d.db.HasBlob(hash)
|
return d.db.HasBlob(hash, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the blob
|
// Get gets the blob
|
||||||
func (d *DBBackedStore) Get(hash string) (stream.Blob, error) {
|
func (d *DBBackedStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) {
|
||||||
has, err := d.db.HasBlob(hash)
|
start := time.Now()
|
||||||
|
has, err := d.db.HasBlob(hash, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), d.Name()), err
|
||||||
}
|
}
|
||||||
if !has {
|
if !has {
|
||||||
return nil, ErrBlobNotFound
|
return nil, shared.NewBlobTrace(time.Since(start), d.Name()), ErrBlobNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.blobs.Get(hash)
|
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 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.
|
// Put stores the blob in the S3 store and stores the blob information in the DB.
|
||||||
|
@ -100,22 +112,22 @@ func (d *DBBackedStore) Block(hash string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
has, err := d.db.HasBlob(hash)
|
//has, err := d.db.HasBlob(hash, false)
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
if has {
|
//if has {
|
||||||
err = d.blobs.Delete(hash)
|
// err = d.blobs.Delete(hash)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
err = d.db.Delete(hash)
|
// err = d.db.Delete(hash)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
return d.markBlocked(hash)
|
return d.markBlocked(hash)
|
||||||
}
|
}
|
||||||
|
@ -182,3 +194,8 @@ func (d *DBBackedStore) initBlocked() error {
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (d *DBBackedStore) Shutdown() {
|
||||||
|
d.blobs.Shutdown()
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
"github.com/lbryio/reflector.go/store/speedwalk"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
"github.com/lbryio/reflector.go/store/speedwalk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DiskStore stores blobs on a local disk
|
// DiskStore stores blobs on a local disk
|
||||||
|
@ -52,37 +54,21 @@ func (d *DiskStore) Has(hash string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the blob or an error if the blob doesn't exist.
|
// 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()
|
err := d.initOnce()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), d.Name()), err
|
||||||
}
|
}
|
||||||
|
|
||||||
blob, err := ioutil.ReadFile(d.path(hash))
|
blob, err := os.ReadFile(d.path(hash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, errors.Err(ErrBlobNotFound)
|
return nil, shared.NewBlobTrace(time.Since(start), d.Name()), errors.Err(ErrBlobNotFound)
|
||||||
}
|
}
|
||||||
return nil, errors.Err(err)
|
return nil, shared.NewBlobTrace(time.Since(start), d.Name()), errors.Err(err)
|
||||||
}
|
}
|
||||||
|
return blob, shared.NewBlobTrace(time.Since(start), d.Name()), nil
|
||||||
return blob, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put stores the blob on disk
|
|
||||||
func (d *DiskStore) Put(hash string, blob stream.Blob) error {
|
|
||||||
err := d.initOnce()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.ensureDirExists(d.dir(hash))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ioutil.WriteFile(d.path(hash), blob, 0644)
|
|
||||||
return errors.Err(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutSD stores the sd blob on the disk
|
// PutSD stores the sd blob on the disk
|
||||||
|
@ -125,11 +111,15 @@ func (d *DiskStore) dir(hash string) string {
|
||||||
}
|
}
|
||||||
return path.Join(d.blobDir, hash[:d.prefixLength])
|
return path.Join(d.blobDir, hash[:d.prefixLength])
|
||||||
}
|
}
|
||||||
|
func (d *DiskStore) tmpDir(hash string) string {
|
||||||
|
return path.Join(d.blobDir, "tmp")
|
||||||
|
}
|
||||||
func (d *DiskStore) path(hash string) string {
|
func (d *DiskStore) path(hash string) string {
|
||||||
return path.Join(d.dir(hash), hash)
|
return path.Join(d.dir(hash), hash)
|
||||||
}
|
}
|
||||||
|
func (d *DiskStore) tmpPath(hash string) string {
|
||||||
|
return path.Join(d.tmpDir(hash), hash)
|
||||||
|
}
|
||||||
func (d *DiskStore) ensureDirExists(dir string) error {
|
func (d *DiskStore) ensureDirExists(dir string) error {
|
||||||
return errors.Err(os.MkdirAll(dir, 0755))
|
return errors.Err(os.MkdirAll(dir, 0755))
|
||||||
}
|
}
|
||||||
|
@ -143,7 +133,14 @@ func (d *DiskStore) initOnce() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = d.ensureDirExists(path.Join(d.blobDir, "tmp"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
d.initialized = true
|
d.initialized = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (d *DiskStore) Shutdown() {
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -14,32 +13,32 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiskStore_Get(t *testing.T) {
|
func TestDiskStore_Get(t *testing.T) {
|
||||||
tmpDir, err := ioutil.TempDir("", "reflector_test_*")
|
tmpDir, err := os.MkdirTemp("", "reflector_test_*")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
d := NewDiskStore(tmpDir, 2)
|
d := NewDiskStore(tmpDir, 2)
|
||||||
|
|
||||||
hash := "1234567890"
|
hash := "f428b8265d65dad7f8ffa52922bba836404cbd62f3ecfe10adba6b444f8f658938e54f5981ac4de39644d5b93d89a94b"
|
||||||
data := []byte("oyuntyausntoyaunpdoyruoyduanrstjwfjyuwf")
|
data := []byte("oyuntyausntoyaunpdoyruoyduanrstjwfjyuwf")
|
||||||
|
|
||||||
expectedPath := path.Join(tmpDir, hash[:2], hash)
|
expectedPath := path.Join(tmpDir, hash[:2], hash)
|
||||||
err = os.MkdirAll(filepath.Dir(expectedPath), os.ModePerm)
|
err = os.MkdirAll(filepath.Dir(expectedPath), os.ModePerm)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = ioutil.WriteFile(expectedPath, data, os.ModePerm)
|
err = os.WriteFile(expectedPath, data, os.ModePerm)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
blob, err := d.Get(hash)
|
blob, _, err := d.Get(hash)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, data, blob)
|
assert.EqualValues(t, data, blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiskStore_GetNonexistentBlob(t *testing.T) {
|
func TestDiskStore_GetNonexistentBlob(t *testing.T) {
|
||||||
tmpDir, err := ioutil.TempDir("", "reflector_test_*")
|
tmpDir, err := os.MkdirTemp("", "reflector_test_*")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
d := NewDiskStore(tmpDir, 2)
|
d := NewDiskStore(tmpDir, 2)
|
||||||
|
|
||||||
blob, err := d.Get("nonexistent")
|
blob, _, err := d.Get("nonexistent")
|
||||||
assert.Nil(t, blob)
|
assert.Nil(t, blob)
|
||||||
assert.True(t, errors.Is(err, ErrBlobNotFound))
|
assert.True(t, errors.Is(err, ErrBlobNotFound))
|
||||||
}
|
}
|
||||||
|
|
42
store/diskstore_put_darwin.go
Normal file
42
store/diskstore_put_darwin.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
var openFileFlags = os.O_WRONLY | os.O_CREATE
|
||||||
|
|
||||||
|
// Put stores the blob on disk
|
||||||
|
func (d *DiskStore) Put(hash string, blob stream.Blob) error {
|
||||||
|
err := d.initOnce()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.ensureDirExists(d.dir(hash))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file with O_DIRECT
|
||||||
|
f, err := os.OpenFile(d.tmpPath(hash), openFileFlags, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(f, bytes.NewReader(blob))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
err = os.Rename(d.tmpPath(hash), d.path(hash))
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
49
store/diskstore_put_linux.go
Normal file
49
store/diskstore_put_linux.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
|
||||||
|
"github.com/brk0v/directio"
|
||||||
|
)
|
||||||
|
|
||||||
|
var openFileFlags = os.O_WRONLY | os.O_CREATE | syscall.O_DIRECT
|
||||||
|
|
||||||
|
// Put stores the blob on disk
|
||||||
|
func (d *DiskStore) Put(hash string, blob stream.Blob) error {
|
||||||
|
err := d.initOnce()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.ensureDirExists(d.dir(hash))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file with O_DIRECT
|
||||||
|
f, err := os.OpenFile(d.tmpPath(hash), openFileFlags, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
dio, err := directio.New(f)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = dio.Flush() }()
|
||||||
|
_, err = io.Copy(dio, bytes.NewReader(blob))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
||||||
|
err = os.Rename(d.tmpPath(hash), d.path(hash))
|
||||||
|
return errors.Err(err)
|
||||||
|
}
|
163
store/gcache.go
Normal file
163
store/gcache.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
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/bluele/gcache"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GcacheStore adds a max cache size and Greedy-Dual-Size-Frequency cache eviction strategy to a BlobStore
|
||||||
|
type GcacheStore struct {
|
||||||
|
// underlying store
|
||||||
|
store BlobStore
|
||||||
|
// cache implementation
|
||||||
|
cache gcache.Cache
|
||||||
|
}
|
||||||
|
type EvictionStrategy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
//LFU Discards the least frequently used items first.
|
||||||
|
LFU EvictionStrategy = iota
|
||||||
|
//ARC Constantly balances between LRU and LFU, to improve the combined result.
|
||||||
|
ARC
|
||||||
|
//LRU Discards the least recently used items first.
|
||||||
|
LRU
|
||||||
|
//SIMPLE has no clear priority for evict cache. It depends on key-value map order.
|
||||||
|
SIMPLE
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGcacheStore initialize a new LRUStore
|
||||||
|
func NewGcacheStore(component string, store BlobStore, maxSize int, strategy EvictionStrategy) *GcacheStore {
|
||||||
|
cacheBuilder := gcache.New(maxSize)
|
||||||
|
var cache gcache.Cache
|
||||||
|
evictFunc := func(key interface{}, value interface{}) {
|
||||||
|
logrus.Infof("evicting %s", key)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
switch strategy {
|
||||||
|
case LFU:
|
||||||
|
cache = cacheBuilder.LFU().EvictedFunc(evictFunc).Build()
|
||||||
|
case ARC:
|
||||||
|
cache = cacheBuilder.ARC().EvictedFunc(evictFunc).Build()
|
||||||
|
case LRU:
|
||||||
|
cache = cacheBuilder.LRU().EvictedFunc(evictFunc).Build()
|
||||||
|
case SIMPLE:
|
||||||
|
cache = cacheBuilder.Simple().EvictedFunc(evictFunc).Build()
|
||||||
|
|
||||||
|
}
|
||||||
|
l := &GcacheStore{
|
||||||
|
store: store,
|
||||||
|
cache: cache,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if lstr, ok := store.(lister); ok {
|
||||||
|
err := l.loadExisting(lstr, maxSize)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // TODO: what should happen here? panic? return nil? just keep going?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameGcache = "gcache"
|
||||||
|
|
||||||
|
// Name is the cache type name
|
||||||
|
func (l *GcacheStore) Name() string { return nameGcache }
|
||||||
|
|
||||||
|
// Has returns whether the blob is in the store, without updating the recent-ness.
|
||||||
|
func (l *GcacheStore) Has(hash string) (bool, error) {
|
||||||
|
return l.cache.Has(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the blob or an error if the blob doesn't exist.
|
||||||
|
func (l *GcacheStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) {
|
||||||
|
start := time.Now()
|
||||||
|
_, err := l.cache.Get(hash)
|
||||||
|
if err != nil {
|
||||||
|
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.cache.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 *GcacheStore) Put(hash string, blob stream.Blob) error {
|
||||||
|
_ = l.cache.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 *GcacheStore) PutSD(hash string, blob stream.Blob) error {
|
||||||
|
_ = l.cache.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 *GcacheStore) 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.cache.Remove(hash)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadExisting imports existing blobs from the underlying store into the LRU cache
|
||||||
|
func (l *GcacheStore) 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 i, h := range existing {
|
||||||
|
_ = l.cache.Set(h, true)
|
||||||
|
added++
|
||||||
|
if maxItems > 0 && added >= maxItems { // underlying cache is bigger than the cache
|
||||||
|
err := l.Delete(h)
|
||||||
|
logrus.Infof("deleted overflowing blob: %s (%d/%d)", h, i, len(existing))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("error while deleting a blob that's overflowing the cache: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (l *GcacheStore) Shutdown() {
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
|
||||||
|
@ -12,96 +13,83 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cacheMaxBlobs = 3
|
const cacheMaxSize = 3
|
||||||
|
|
||||||
func getTestLRUStore() (*LRUStore, *MemStore) {
|
func getTestGcacheStore() (*GcacheStore, *MemStore) {
|
||||||
m := NewMemStore()
|
m := NewMemStore()
|
||||||
return NewLRUStore("test", m, 3), m
|
return NewGcacheStore("test", m, cacheMaxSize, LFU), m
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUStore_Eviction(t *testing.T) {
|
func TestGcacheStore_Eviction(t *testing.T) {
|
||||||
lru, mem := getTestLRUStore()
|
lfu, mem := getTestGcacheStore()
|
||||||
b := []byte("x")
|
b := []byte("x")
|
||||||
err := lru.Put("one", b)
|
for i := 0; i < 3; i++ {
|
||||||
require.NoError(t, err)
|
err := lfu.Put(fmt.Sprintf("%d", i), b)
|
||||||
err = lru.Put("two", b)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
for j := 0; j < 3-i; j++ {
|
||||||
err = lru.Put("three", b)
|
_, _, err = lfu.Get(fmt.Sprintf("%d", i))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = lru.Put("four", b)
|
}
|
||||||
require.NoError(t, err)
|
}
|
||||||
err = lru.Put("five", b)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, cacheMaxBlobs, len(mem.Debug()))
|
|
||||||
|
|
||||||
for k, v := range map[string]bool{
|
for k, v := range map[string]bool{
|
||||||
"one": false,
|
"0": true,
|
||||||
"two": false,
|
"1": true,
|
||||||
"three": true,
|
"2": true,
|
||||||
"four": true,
|
|
||||||
"five": true,
|
|
||||||
"six": false,
|
|
||||||
} {
|
} {
|
||||||
has, err := lru.Has(k)
|
has, err := lfu.Has(k)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, v, has)
|
assert.Equal(t, v, has)
|
||||||
}
|
}
|
||||||
|
err := lfu.Put("3", b)
|
||||||
lru.Get("three") // touch so it stays in cache
|
require.NoError(t, err)
|
||||||
lru.Put("six", b)
|
|
||||||
|
|
||||||
assert.Equal(t, cacheMaxBlobs, len(mem.Debug()))
|
|
||||||
|
|
||||||
for k, v := range map[string]bool{
|
for k, v := range map[string]bool{
|
||||||
"one": false,
|
"0": true,
|
||||||
"two": false,
|
"1": true,
|
||||||
"three": true,
|
"2": false,
|
||||||
"four": false,
|
"3": true,
|
||||||
"five": true,
|
|
||||||
"six": true,
|
|
||||||
} {
|
} {
|
||||||
has, err := lru.Has(k)
|
has, err := lfu.Has(k)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, v, has)
|
assert.Equal(t, v, has)
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, cacheMaxSize, len(mem.Debug()))
|
||||||
|
|
||||||
err = lru.Delete("three")
|
err = lfu.Delete("0")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = lru.Delete("five")
|
err = lfu.Delete("1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = lru.Delete("six")
|
err = lfu.Delete("3")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, len(mem.Debug()))
|
assert.Equal(t, 0, len(mem.Debug()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUStore_UnderlyingBlobMissing(t *testing.T) {
|
func TestGcacheStore_UnderlyingBlobMissing(t *testing.T) {
|
||||||
lru, mem := getTestLRUStore()
|
lfu, mem := getTestGcacheStore()
|
||||||
hash := "hash"
|
hash := "hash"
|
||||||
b := []byte("this is a blob of stuff")
|
b := []byte("this is a blob of stuff")
|
||||||
err := lru.Put(hash, b)
|
err := lfu.Put(hash, b)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = mem.Delete(hash)
|
err = mem.Delete(hash)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// hash still exists in lru
|
// hash still exists in lru
|
||||||
assert.True(t, lru.lru.Contains(hash))
|
assert.True(t, lfu.cache.Has(hash))
|
||||||
|
|
||||||
blob, err := lru.Get(hash)
|
blob, _, err := lfu.Get(hash)
|
||||||
assert.Nil(t, blob)
|
assert.Nil(t, blob)
|
||||||
assert.True(t, errors.Is(err, ErrBlobNotFound), "expected (%s) %s, got (%s) %s",
|
assert.True(t, errors.Is(err, ErrBlobNotFound), "expected (%s) %s, got (%s) %s",
|
||||||
reflect.TypeOf(ErrBlobNotFound).String(), ErrBlobNotFound.Error(),
|
reflect.TypeOf(ErrBlobNotFound).String(), ErrBlobNotFound.Error(),
|
||||||
reflect.TypeOf(err).String(), err.Error())
|
reflect.TypeOf(err).String(), err.Error())
|
||||||
|
|
||||||
// lru.Get() removes hash if underlying store doesn't have it
|
// lru.Get() removes hash if underlying store doesn't have it
|
||||||
assert.False(t, lru.lru.Contains(hash))
|
assert.False(t, lfu.cache.Has(hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUStore_loadExisting(t *testing.T) {
|
func TestGcacheStore_loadExisting(t *testing.T) {
|
||||||
tmpDir, err := ioutil.TempDir("", "reflector_test_*")
|
tmpDir, err := os.MkdirTemp("", "reflector_test_*")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
d := NewDiskStore(tmpDir, 2)
|
d := NewDiskStore(tmpDir, 2)
|
||||||
|
|
||||||
hash := "hash"
|
hash := "hash"
|
||||||
|
@ -114,8 +102,9 @@ func TestLRUStore_loadExisting(t *testing.T) {
|
||||||
require.Equal(t, 1, len(existing), "blob should exist in cache")
|
require.Equal(t, 1, len(existing), "blob should exist in cache")
|
||||||
assert.Equal(t, hash, existing[0])
|
assert.Equal(t, hash, existing[0])
|
||||||
|
|
||||||
lru := NewLRUStore("test", d, 3) // lru should load existing blobs when it's created
|
lfu := NewGcacheStore("test", d, 3, LFU) // lru should load existing blobs when it's created
|
||||||
has, err := lru.Has(hash)
|
time.Sleep(100 * time.Millisecond) // async load so let's wait...
|
||||||
|
has, err := lfu.Has(hash)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, has, "hash should be loaded from disk store but it's not")
|
assert.True(t, has, "hash should be loaded from disk store but it's not")
|
||||||
}
|
}
|
170
store/http.go
Normal file
170
store/http.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HttpStore is a store that works on top of the HTTP protocol
|
||||||
|
type HttpStore struct {
|
||||||
|
upstream string
|
||||||
|
httpClient *http.Client
|
||||||
|
edgeToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpStore(upstream, edgeToken string) *HttpStore {
|
||||||
|
return &HttpStore{
|
||||||
|
upstream: "http://" + upstream,
|
||||||
|
httpClient: getClient(),
|
||||||
|
edgeToken: edgeToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameHttp = "http"
|
||||||
|
|
||||||
|
func (n *HttpStore) Name() string { return nameHttp }
|
||||||
|
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 func() { _ = 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, _ = io.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
|
||||||
|
if n.edgeToken != "" {
|
||||||
|
url += "&edge_token=" + n.edgeToken
|
||||||
|
}
|
||||||
|
|
||||||
|
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 func() { _ = 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, _ = io.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() {}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: 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}
|
||||||
|
}
|
73
store/ittt.go
Normal file
73
store/ittt.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ITTTStore performs an operation on this storage, if this fails, it attempts to run it on that
|
||||||
|
type ITTTStore struct {
|
||||||
|
this, that BlobStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewITTTStore returns a new instance of the IF THIS THAN THAT store
|
||||||
|
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 in this for a hash, if it fails it checks in that. 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() {}
|
120
store/lru.go
120
store/lru.go
|
@ -1,120 +0,0 @@
|
||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LRUStore adds a max cache size and LRU eviction to a BlobStore
|
|
||||||
type LRUStore struct {
|
|
||||||
// underlying store
|
|
||||||
store BlobStore
|
|
||||||
// lru implementation
|
|
||||||
lru *golru.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?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameLRU = "lru"
|
|
||||||
|
|
||||||
// Name is the cache type name
|
|
||||||
func (l *LRUStore) Name() string { return nameLRU }
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
blob, err := l.store.Get(hash)
|
|
||||||
if errors.Is(err, ErrBlobNotFound) {
|
|
||||||
// Blob disappeared from underlying store
|
|
||||||
l.lru.Remove(hash)
|
|
||||||
}
|
|
||||||
return blob, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put stores the blob
|
|
||||||
func (l *LRUStore) Put(hash string, blob stream.Blob) error {
|
|
||||||
err := l.store.Put(hash, blob)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.lru.Add(hash, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutSD stores the sd blob
|
|
||||||
func (l *LRUStore) PutSD(hash string, blob stream.Blob) error {
|
|
||||||
err := l.store.PutSD(hash, blob)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.lru.Add(hash, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the blob from the store
|
|
||||||
func (l *LRUStore) 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.lru.Remove(hash)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadExisting imports existing blobs from the underlying store into the LRU cache
|
|
||||||
func (l *LRUStore) loadExisting(store lister, maxItems int) error {
|
|
||||||
existing, err := store.list()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
added := 0
|
|
||||||
for _, h := range existing {
|
|
||||||
l.lru.Add(h, true)
|
|
||||||
added++
|
|
||||||
if maxItems > 0 && added >= maxItems { // underlying cache is bigger than LRU cache
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -2,6 +2,9 @@ package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
@ -34,14 +37,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.
|
// 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()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
blob, ok := m.blobs[hash]
|
blob, ok := m.blobs[hash]
|
||||||
if !ok {
|
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
|
// Put stores the blob in memory
|
||||||
|
@ -71,3 +75,6 @@ func (m *MemStore) Debug() map[string]stream.Blob {
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
return m.blobs
|
return m.blobs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (m *MemStore) Shutdown() {}
|
||||||
|
|
|
@ -25,7 +25,7 @@ func TestMemStore_Get(t *testing.T) {
|
||||||
t.Error("error getting memory blob - ", err)
|
t.Error("error getting memory blob - ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotBlob, err := s.Get(hash)
|
gotBlob, _, err := s.Get(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Expected no error, got %v", err)
|
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")
|
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 {
|
if err == nil {
|
||||||
t.Errorf("Expected ErrBlobNotFound, got nil")
|
t.Errorf("Expected ErrBlobNotFound, got nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import "github.com/lbryio/lbry.go/v2/stream"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
|
)
|
||||||
|
|
||||||
// NoopStore is a store that does nothing
|
// NoopStore is a store that does nothing
|
||||||
type NoopStore struct{}
|
type NoopStore struct{}
|
||||||
|
|
||||||
const nameNoop = "noop"
|
const nameNoop = "noop"
|
||||||
|
|
||||||
func (n *NoopStore) Name() string { return nameNoop }
|
func (n *NoopStore) Name() string { return nameNoop }
|
||||||
func (n *NoopStore) Has(_ string) (bool, error) { return false, nil }
|
func (n *NoopStore) Has(_ string) (bool, error) { return false, nil }
|
||||||
func (n *NoopStore) Get(_ string) (stream.Blob, error) { return nil, 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) Put(_ string, _ stream.Blob) error { return nil }
|
||||||
func (n *NoopStore) PutSD(_ 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) Delete(_ string) error { return nil }
|
||||||
|
func (n *NoopStore) Shutdown() { return }
|
||||||
|
|
34
store/s3.go
34
store/s3.go
|
@ -5,9 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
|
@ -65,17 +67,18 @@ func (s *S3Store) Has(hash string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the blob slice if present or errors on S3.
|
// 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.
|
//Todo-Need to handle error for blob doesn't exist for consistency.
|
||||||
err := s.initOnce()
|
err := s.initOnce()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, shared.NewBlobTrace(time.Since(start), s.Name()), err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Getting %s from S3", hash[:8])
|
log.Debugf("Getting %s from S3", hash[:8])
|
||||||
defer func(t time.Time) {
|
defer func(t time.Time) {
|
||||||
log.Debugf("Getting %s from S3 took %s", hash[:8], time.Since(t).String())
|
log.Debugf("Getting %s from S3 took %s", hash[:8], time.Since(t).String())
|
||||||
}(time.Now())
|
}(start)
|
||||||
|
|
||||||
buf := &aws.WriteAtBuffer{}
|
buf := &aws.WriteAtBuffer{}
|
||||||
_, err = s3manager.NewDownloader(s.session).Download(buf, &s3.GetObjectInput{
|
_, err = s3manager.NewDownloader(s.session).Download(buf, &s3.GetObjectInput{
|
||||||
|
@ -86,15 +89,15 @@ func (s *S3Store) Get(hash string) (stream.Blob, error) {
|
||||||
if aerr, ok := err.(awserr.Error); ok {
|
if aerr, ok := err.(awserr.Error); ok {
|
||||||
switch aerr.Code() {
|
switch aerr.Code() {
|
||||||
case s3.ErrCodeNoSuchBucket:
|
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:
|
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.
|
// Put stores the blob on S3 or errors if S3 connection errors.
|
||||||
|
@ -110,10 +113,11 @@ func (s *S3Store) Put(hash string, blob stream.Blob) error {
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
_, err = s3manager.NewUploader(s.session).Upload(&s3manager.UploadInput{
|
_, err = s3manager.NewUploader(s.session).Upload(&s3manager.UploadInput{
|
||||||
Bucket: aws.String(s.bucket),
|
Bucket: aws.String(s.bucket),
|
||||||
Key: aws.String(hash),
|
Key: aws.String(hash),
|
||||||
Body: bytes.NewBuffer(blob),
|
Body: bytes.NewBuffer(blob),
|
||||||
StorageClass: aws.String(s3.StorageClassIntelligentTiering),
|
ACL: aws.String("public-read"),
|
||||||
|
//StorageClass: aws.String(s3.StorageClassIntelligentTiering),
|
||||||
})
|
})
|
||||||
metrics.MtrOutBytesReflector.Add(float64(blob.Size()))
|
metrics.MtrOutBytesReflector.Add(float64(blob.Size()))
|
||||||
|
|
||||||
|
@ -150,6 +154,7 @@ func (s *S3Store) initOnce() error {
|
||||||
sess, err := session.NewSession(&aws.Config{
|
sess, err := session.NewSession(&aws.Config{
|
||||||
Credentials: credentials.NewStaticCredentials(s.awsID, s.awsSecret, ""),
|
Credentials: credentials.NewStaticCredentials(s.awsID, s.awsSecret, ""),
|
||||||
Region: aws.String(s.region),
|
Region: aws.String(s.region),
|
||||||
|
Endpoint: aws.String("https://s3.wasabisys.com"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -158,3 +163,8 @@ func (s *S3Store) initOnce() error {
|
||||||
s.session = sess
|
s.session = sess
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (s *S3Store) Shutdown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/reflector.go/internal/metrics"
|
"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/lbry.go/v2/stream"
|
||||||
|
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
|
@ -29,39 +31,98 @@ func (s *singleflightStore) Name() string {
|
||||||
return "sf_" + s.BlobStore.Name()
|
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,
|
// 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
|
// thereby protecting against https://en.wikipedia.org/wiki/Thundering_herd_problem
|
||||||
func (s *singleflightStore) Get(hash string) (stream.Blob, error) {
|
func (s *singleflightStore) Get(hash string) (stream.Blob, shared.BlobTrace, error) {
|
||||||
metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Inc()
|
start := time.Now()
|
||||||
defer metrics.CacheWaitingRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Dec()
|
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 {
|
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
|
// getter returns a function that gets a blob from the origin
|
||||||
// only one getter per hash will be executing at a time
|
// only one getter per hash will be executing at a time
|
||||||
func (s *singleflightStore) getter(hash string) func() (interface{}, error) {
|
func (s *singleflightStore) getter(hash string) func() (interface{}, error) {
|
||||||
return func() (interface{}, error) {
|
return func() (interface{}, error) {
|
||||||
metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Inc()
|
metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Inc()
|
||||||
defer metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.BlobStore.Name(), s.component)).Dec()
|
defer metrics.CacheOriginRequestsCount.With(metrics.CacheLabels(s.Name(), s.component)).Dec()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
blob, err := s.BlobStore.Get(hash)
|
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()
|
||||||
|
err := s.BlobStore.Put(hash, blob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds()
|
rate := float64(len(blob)) / 1024 / 1024 / time.Since(start).Seconds()
|
||||||
metrics.CacheRetrievalSpeed.With(map[string]string{
|
metrics.CacheRetrievalSpeed.With(map[string]string{
|
||||||
metrics.LabelCacheType: s.BlobStore.Name(),
|
metrics.LabelCacheType: s.Name(),
|
||||||
metrics.LabelComponent: s.component,
|
metrics.LabelComponent: s.component,
|
||||||
metrics.LabelSource: "origin",
|
metrics.LabelSource: "origin",
|
||||||
}).Set(rate)
|
}).Set(rate)
|
||||||
|
|
||||||
return blob, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown shuts down the store gracefully
|
||||||
|
func (s *singleflightStore) Shutdown() {
|
||||||
|
s.BlobStore.Shutdown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package speedwalk
|
package speedwalk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/lbryio/reflector.go/internal/metrics"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
|
|
||||||
"github.com/karrick/godirwalk"
|
"github.com/karrick/godirwalk"
|
||||||
|
@ -15,7 +18,19 @@ import (
|
||||||
// AllFiles recursively lists every file in every subdirectory of a given directory
|
// AllFiles recursively lists every file in every subdirectory of a given directory
|
||||||
// If basename is true, return the basename of each file. Otherwise return the full path starting at startDir.
|
// If basename is true, return the basename of each file. Otherwise return the full path starting at startDir.
|
||||||
func AllFiles(startDir string, basename bool) ([]string, error) {
|
func AllFiles(startDir string, basename bool) ([]string, error) {
|
||||||
items, err := ioutil.ReadDir(startDir)
|
entries, err := os.ReadDir(startDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]fs.FileInfo, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, info)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -24,6 +39,7 @@ func AllFiles(startDir string, basename bool) ([]string, error) {
|
||||||
paths := make([]string, 0, 1000)
|
paths := make([]string, 0, 1000)
|
||||||
pathWG := &sync.WaitGroup{}
|
pathWG := &sync.WaitGroup{}
|
||||||
pathWG.Add(1)
|
pathWG.Add(1)
|
||||||
|
metrics.RoutinesQueue.WithLabelValues("speedwalk", "worker").Inc()
|
||||||
go func() {
|
go func() {
|
||||||
defer pathWG.Done()
|
defer pathWG.Done()
|
||||||
for {
|
for {
|
||||||
|
@ -60,7 +76,6 @@ func AllFiles(startDir string, basename bool) ([]string, error) {
|
||||||
walkerWG.Done()
|
walkerWG.Done()
|
||||||
goroutineLimiter <- struct{}{}
|
goroutineLimiter <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = godirwalk.Walk(filepath.Join(startDir, dir), &godirwalk.Options{
|
err = godirwalk.Walk(filepath.Join(startDir, dir), &godirwalk.Options{
|
||||||
Unsorted: true, // faster this way
|
Unsorted: true, // faster this way
|
||||||
Callback: func(osPathname string, de *godirwalk.Dirent) error {
|
Callback: func(osPathname string, de *godirwalk.Dirent) error {
|
||||||
|
@ -84,6 +99,5 @@ func AllFiles(startDir string, basename bool) ([]string, error) {
|
||||||
|
|
||||||
close(pathChan)
|
close(pathChan)
|
||||||
pathWG.Wait()
|
pathWG.Wait()
|
||||||
|
|
||||||
return paths, nil
|
return paths, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lbryio/reflector.go/shared"
|
||||||
|
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
||||||
"github.com/lbryio/lbry.go/v2/stream"
|
"github.com/lbryio/lbry.go/v2/stream"
|
||||||
)
|
)
|
||||||
|
@ -9,16 +11,18 @@ import (
|
||||||
type BlobStore interface {
|
type BlobStore interface {
|
||||||
// Name of blob store (useful for metrics)
|
// Name of blob store (useful for metrics)
|
||||||
Name() string
|
Name() string
|
||||||
// Does blob exist in the store.
|
// Has Does blob exist in the store.
|
||||||
Has(hash string) (bool, error)
|
Has(hash string) (bool, error)
|
||||||
// Get the blob from the store. Must return ErrBlobNotFound if blob is not in store.
|
// 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 the blob into the store.
|
||||||
Put(hash string, blob stream.Blob) error
|
Put(hash string, blob stream.Blob) error
|
||||||
// Put an SD blob into the store.
|
// PutSD an SD blob into the store.
|
||||||
PutSD(hash string, blob stream.Blob) error
|
PutSD(hash string, blob stream.Blob) error
|
||||||
// Delete the blob from the store.
|
// Delete the blob from the store.
|
||||||
Delete(hash string) error
|
Delete(hash string) error
|
||||||
|
// Shutdown the store gracefully
|
||||||
|
Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocklister is a store that supports blocking blobs to prevent their inclusion in the store.
|
// Blocklister is a store that supports blocking blobs to prevent their inclusion in the store.
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/lbryio/chainquery/lbrycrd"
|
"github.com/lbryio/chainquery/lbrycrd"
|
||||||
"github.com/lbryio/lbry.go/v2/extras/errors"
|
"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"
|
types "github.com/lbryio/types/v2/go"
|
||||||
|
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
|
@ -140,7 +140,7 @@ func (n *Node) GetClaimInTx(txid string, nout int) (*types.Claim, error) {
|
||||||
return nil, errors.Err(err)
|
return nil, errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := claim.DecodeClaimBytes(value, "")
|
ch, err := stake.DecodeClaimBytes(value, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Err(err)
|
return nil, errors.Err(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue