Compare commits

...

101 commits

Author SHA1 Message Date
Niko Storni
4d81a43a8f update dependencies 2023-10-11 21:01:37 +02:00
Niko Storni
b3f0d63b4d fix bug 2023-10-11 20:52:13 +02:00
Niko Storni
c880f0b80f fix bug rendering sf useless 2023-10-11 20:45:31 +02:00
Niko Storni
085490e92b upgrade go version
update dependencies
2023-07-07 15:34:22 +02:00
Niko
456fe53e01
Merge pull request #67 from lbryio/upgrades
upgrade dependencies & introduce github ci
2023-03-09 13:12:59 -05:00
Niko Storni
778fc17adf update readme 2023-03-09 18:59:21 +01:00
Niko Storni
e93c097fd9 upgrade dependencies
replace deprecated function calls
refactor build process
2023-03-09 18:41:41 +01:00
Niko
0dfda70c70
Merge pull request #63 from lbryio/declare_err
Add undeclared errors from peer.go to metrics
2022-10-17 19:01:56 +02:00
Victor Shyba
6c082993cf metrics: declare request is too large 2022-10-14 15:21:06 -03:00
Victor Shyba
08ed3c9f13 metrics: declare invalid blob hash len 2022-10-14 15:18:46 -03:00
Victor Shyba
7f75602841 metrics: declare protected blob 2022-10-14 15:16:54 -03:00
Victor Shyba
4d168ddefc metrics: declare invalid json request 2022-10-14 15:15:32 -03:00
Niko
b7abb77ea1
Merge pull request #62 from lbryio/declare_err
declare errInvalidPeerData on metrics
2022-10-14 19:45:40 +02:00
Victor Shyba
8c2a46752c declare errInvalidPeerData on metrics 2022-10-14 14:42:22 -03:00
Niko Storni
0c8da4abe5 Merge remote-tracking branch 'origin/peer_proto_tests' 2022-10-14 18:30:08 +02:00
Victor Shyba
87680d806c check for valid data earlier. reply composite as tests are expecting 2022-10-11 22:58:41 -03:00
Victor Shyba
e1c59b9b63 test through the connection 2022-10-11 22:57:52 -03:00
Niko Storni
c79634706b ignore server closed errors the right way 2022-09-23 14:46:55 +02:00
Niko Storni
f5d30b1a6e update quic-go 2022-07-30 20:10:28 +02:00
Niko Storni
0177dd4ce0 fix query param 2022-07-29 05:40:50 +02:00
Niko Storni
5693529216 protect protected content 2022-07-29 04:59:15 +02:00
Niko
a1c2e92ca3
Merge pull request #57 from andybeletsky/fix-store-macos
Do not use direction package on macos
2022-05-12 16:01:00 +02:00
Andrey Beletsky
9c0554ef05 Do not use direction package on macos 2022-05-08 15:39:34 +07:00
Niko Storni
4e80f91a57 fix memory leak 2022-05-04 19:14:20 +02:00
Niko Storni
c211f83ba7 update dependencies 2022-05-02 23:32:36 +02:00
Niko Storni
29d1ccf68c add singleflight to web requests 2022-05-02 23:07:22 +02:00
Niko Storni
2f7d67794f remove debug leftover 2022-02-10 00:57:13 +01:00
Niko Storni
4d8e7739d7 Merge branch 'fix-ci' 2021-12-14 22:36:01 +01:00
Niko Storni
6fc0ceea2a adjust travis vars
fix build script


fix more scripts


adapt script to scale
2021-12-14 22:35:44 +01:00
Niko Storni
ae0c7dd2bb upgrade quic-go for go1.17 2021-12-14 20:49:50 +01:00
Niko Storni
4af5c2f4c6 make slack channel configurable 2021-10-30 00:21:58 +02:00
Alex Grin
def0a97f49
Update readme.md 2021-09-28 10:15:24 -04:00
Niko Storni
6dde793745 do not delete blobs for blocked content 2021-09-21 18:00:55 +02:00
Niko Storni
654cc44935 Merge branch 'hash_twice' 2021-09-21 16:21:25 +02:00
Victor Shyba
90d6d29452 remove all hashing on the download path 2021-09-21 16:19:49 +02:00
Niko Storni
b2272fef3a delete overflowing blobs from underlying cache 2021-08-18 19:36:47 +02:00
Niko Storni
86f3e62aa8 fix a panic error
update gin-go
2021-08-05 17:47:20 +02:00
Niko
e1b4f21e00
Merge pull request #52 from lbryio/ittt
Merge months of work including all sort of caches and O_DIRECT optimizations
2021-07-23 20:35:22 -04:00
Niko Storni
b4913ecedf cleanup 2021-07-24 01:03:51 +02:00
Niko Storni
63a574ec2f unify caches
fix tests
2021-07-24 01:03:51 +02:00
Niko Storni
b8af3408e0 move server packages 2021-07-24 01:03:51 +02:00
Niko Storni
847089d0d6 fix error propagation
update readme
2021-07-24 01:03:51 +02:00
Niko Storni
170dfef3a8 fix copy pasta mistake 2021-07-24 01:03:51 +02:00
Niko Storni
2b458a6bd0 fix params
more cleanups
2021-07-24 01:03:51 +02:00
Niko Storni
febfc51cb0 refactor refactor refactor 2021-07-24 01:03:51 +02:00
Andrey Beletsky
72be487262 Fix broken import 2021-07-24 01:03:51 +02:00
Andrey Beletsky
94e7d81bd3 Fix OpenFile call flags for macos 2021-07-24 01:03:51 +02:00
Niko Storni
c6c779da39 fix panic
fix counter leak
2021-07-24 01:03:51 +02:00
Niko Storni
2e101083e6 write blobs to tmp dir to avoid corruption 2021-07-24 01:03:51 +02:00
Niko Storni
63aacd8a69 use O_DIRECT to write to disk (fixes everything)
add queue back to serving blobs
improve a lot of things
upgrade modules
2021-07-24 01:03:51 +02:00
Niko Storni
c03ae6487d fix unsafe dereference 2021-07-24 01:03:51 +02:00
Niko Storni
0c4f455f0c add metrics 2021-07-24 01:03:51 +02:00
Niko Storni
af3e08c446 update lbry.go dep 2021-07-24 01:03:51 +02:00
Niko Storni
975bfe7fac upgrade singleflight
http store fix
2021-07-24 01:03:51 +02:00
Niko Storni
b075d948bb remove locks causing deadlocks 2021-07-24 01:03:51 +02:00
Niko Storni
2651a64dbb add http server/client 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
fa7150cf2b Add queue to prevent writing too many files at once. 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
6c4db980c9 Add queue to prevent writing too many files at once. 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
7adaa510fd Add locks to disk store. 2021-07-24 01:03:51 +02:00
Niko Storni
64ed7304f6 add a lot of extra heavy debugging 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
5aefaf061e Add single flight for cache not just origin 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
724ee47c8b add metric calls for other packages 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
caaec6fcb1 add guage metrics for go routines in reflector package 2021-07-24 01:03:51 +02:00
Mark Beamer Jr
15984b8fd9 add gops to reflector server 2021-07-24 01:03:51 +02:00
Niko Storni
2be913b077 request queue size param 2021-07-24 01:03:51 +02:00
Niko Storni
34c11b0a0e increase window size 2021-07-24 01:03:51 +02:00
Niko Storni
64acdc29c3 improve disk cleanup
add index to is_stored
fix test
replace LRU cache
2021-07-24 01:03:51 +02:00
Niko Storni
598773c90d fix mess with lbry.go 2021-07-24 01:03:47 +02:00
Niko Storni
766238fd7e add if this than that store
switch to wasabi for uploads
2021-07-24 01:03:47 +02:00
Niko Storni
ac5242f173 add integrity check cmd
throttle live integrity checks
bug fixes
2021-07-24 01:03:47 +02:00
Mark Beamer Jr
215103cb33 use wait group not stopper 2021-07-24 01:03:47 +02:00
Mark Beamer Jr
ed3622d0a6 Wait for request to be handled before returning 2021-07-24 01:03:47 +02:00
Mark Beamer Jr
848fce5afa Add request queue for blob cache 2021-07-24 01:03:47 +02:00
Niko Storni
e37eeba0c9 check blobs when reading them 2021-07-24 01:03:47 +02:00
Niko Storni
7da49a4ccb upgrade quic-go
add cache for blobs not found
2021-07-24 01:03:47 +02:00
Niko Storni
7b02ace5e2 fix issues caused by beamer's renaming 2021-07-24 01:03:47 +02:00
Niko Storni
5fb67b32db run go mod tidy 2021-07-24 01:03:47 +02:00
Niko Storni
a0c9ed2ace make it simpler 2021-07-24 01:03:47 +02:00
Niko Storni
998b082a06 remove panics 2021-07-24 01:03:47 +02:00
Niko Storni
36d4156e2a add tracing to blobs 2021-07-24 01:03:47 +02:00
Niko Storni
74925ebba2 optimize batch insertions
reduce touch time to every 6 hours
2021-07-24 01:03:47 +02:00
Alex Grintsvayg
6f95b3395f avoid heavy interpolateparams call 2021-07-24 01:03:47 +02:00
Alex Grintsvayg
dff00e2317 fix long query 2021-07-24 01:03:47 +02:00
Alex Grintsvayg
9a5d9d7ff5 only touch blobs when you get them 2021-07-24 01:03:47 +02:00
Niko Storni
5794c57898 save uploaded blobs and work around the blocklist issue 2021-07-24 01:03:47 +02:00
Niko Storni
35c713a26e add cmd to populate db
fix store init
try fixing unreasonable db bottleneck
2021-07-24 01:03:47 +02:00
Alex Grintsvayg
6fb0620091 something like this 2021-07-24 01:03:38 +02:00
Niko Storni
03df751bc7 add PoC for litedb to avoid all the overhead 2021-07-24 01:03:16 +02:00
Niko Storni
c902858958 address some review comments 2021-07-24 01:03:16 +02:00
Niko Storni
84fabdd5f4 add option to run with RO-CF only as upstream
increase idle timeout to avoid errors downstream
add option to delete blobs from DB if storage doesn't have it (for future local tracking)
2021-07-24 01:03:16 +02:00
Niko Storni
f5cad15f84 upgrade quic 2021-07-24 01:03:10 +02:00
Niko Storni
dd3d0ae42c update lfuda library 2021-07-24 01:03:10 +02:00
Niko Storni
0b565852b8 only store the blobs in the underlying storage if LFUDA accepted them 2021-07-24 01:03:10 +02:00
Niko Storni
ff13d7b2f7 fix cache size mess 2021-07-24 01:03:10 +02:00
Niko Storni
7f5a89fa5a fix buffer cache running out of space 2021-07-24 01:03:10 +02:00
Niko Storni
704e15f8c1 use LFUDA store
swap size to bytes
2021-07-24 01:03:10 +02:00
Niko Storni
5eb1f13b54 add LFUDA store
update quic
fix tests
2021-07-24 01:03:10 +02:00
Alex Grintsvayg
176e05714e rename cahces 2021-07-24 01:03:10 +02:00
Niko Storni
eefd84b02d add buffer cache for nvme drive 2021-07-24 01:03:10 +02:00
Niko Storni
af2742c34f update quic
don't wait for a blob to be written to disk before sending it downstream
don't wait for the disk store to be walked before starting everything up
2021-07-24 01:03:10 +02:00
Alex Grintsvayg
2cf4acdb59
add 'send' command to efficiently send a file to reflector 2021-04-02 14:30:36 -04:00
74 changed files with 4028 additions and 1122 deletions

37
.github/workflows/go.yml vendored Normal file
View 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
View 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
View file

@ -1,3 +1,4 @@
/vendor /vendor
/config.json* /config.json*
/dist
/bin /bin

View file

@ -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

View file

@ -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" ]

View file

@ -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 .

View file

@ -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)

View file

@ -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"

View file

@ -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)
} }

View file

@ -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
View 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
}

View file

@ -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
View 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))
}
}

View file

@ -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 (
//port configuration
tcpPeerPort int tcpPeerPort int
http3PeerPort int http3PeerPort int
httpPeerPort int
receiverPort int receiverPort int
metricsPort int metricsPort int
//flags configuration
disableUploads bool disableUploads bool
disableBlocklist bool disableBlocklist bool
proxyAddress string
proxyPort string
proxyProtocol string
useDB bool useDB bool
cloudFrontEndpoint string
reflectorCmdDiskCache string //upstream configuration
reflectorCmdMemCache int 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 var s store.BlobStore
if upstreamReflector == "" {
if proxyAddress != "" { return nil
switch proxyProtocol { }
switch upstreamProtocol {
case "tcp": case "tcp":
s = peer.NewStore(peer.StoreOpts{ s = peer.NewStore(peer.StoreOpts{
Address: proxyAddress + ":" + proxyPort, Address: upstreamReflector,
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
}) })
case "http3": case "http3":
s = http3.NewStore(http3.StoreOpts{ s = http3.NewStore(http3.StoreOpts{
Address: proxyAddress + ":" + proxyPort, Address: upstreamReflector,
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
}) })
case "http":
s = store.NewHttpStore(upstreamReflector, upstreamEdgeToken)
default: default:
log.Fatalf("protocol is not recognized: %s", proxyProtocol) log.Fatalf("protocol is not recognized: %s", upstreamProtocol)
} }
return s
}
func initEdgeStore() store.BlobStore {
var s3Store *store.S3Store
var s store.BlobStore
if conf != "none" {
s3Store = store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName)
}
if originEndpointFallback != "" && originEndpoint != "" {
ittt := store.NewITTTStore(store.NewCloudFrontROStore(originEndpoint), store.NewCloudFrontROStore(originEndpointFallback))
if s3Store != nil {
s = store.NewCloudFrontRWStore(ittt, s3Store)
} else { } else {
s3Store := store.NewS3Store(globalConfig.AwsID, globalConfig.AwsSecret, globalConfig.BucketRegion, globalConfig.BucketName) s = ittt
if cloudFrontEndpoint != "" { }
s = store.NewCloudFrontRWStore(store.NewCloudFrontROStore(cloudFrontEndpoint), s3Store) } else if s3Store != nil {
} else {
s = s3Store 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{
TrackAccess: db.TrackAccessStreams,
LogQueries: log.GetLevel() == log.DebugLevel,
}
err := dbInst.Connect(globalConfig.DBConn)
if err != nil {
log.Fatal(err)
}
s = store.NewDBBackedStore(s, dbInst, false)
}
return s
}
diskCacheMaxSize, diskCachePath := diskCacheParams() func initStores() store.BlobStore {
if diskCacheMaxSize > 0 { 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",
finalStore,
store.NewGcacheStore("mem", store.NewMemStore(), memCache, store.LRU),
)
}
return finalStore, stopper
}
func initDiskStore(upstreamStore store.BlobStore, diskParams string, stopper *stop.Group) store.BlobStore {
diskCacheMaxSize, diskCachePath, cacheManager := diskCacheParams(diskParams)
//we are tracking blobs in memory with a 1 byte long boolean, which means that for each 2MB (a blob) we need 1Byte
// so if the underlying cache holds 10MB, 10MB/2MB=5Bytes which is also the exact count of objects to restore on startup
realCacheSize := float64(diskCacheMaxSize) / float64(stream.MaxBlobSize)
if diskCacheMaxSize == 0 {
return upstreamStore
}
err := os.MkdirAll(diskCachePath, os.ModePerm) err := os.MkdirAll(diskCachePath, os.ModePerm)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
wrapped = store.NewCachingStore(
"reflector", diskStore := store.NewDiskStore(diskCachePath, 2)
wrapped, var unwrappedStore store.BlobStore
store.NewLRUStore("peer_server", store.NewDiskStore(diskCachePath, 2), diskCacheMaxSize), 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])
} }
if reflectorCmdMemCache > 0 { wrapped := store.NewCachingStore(
wrapped = store.NewCachingStore(
"reflector", "reflector",
wrapped, upstreamStore,
store.NewLRUStore("peer_server", store.NewMemStore(), reflectorCmdMemCache), 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
} }

View file

@ -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
View 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
}

View file

@ -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{}

View file

@ -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()

View file

@ -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))

View file

@ -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)

View file

@ -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
View file

@ -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
View file

@ -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
)

992
go.sum

File diff suppressed because it is too large Load diff

View file

@ -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,
@ -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
View 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
*/

View file

@ -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>"

View file

@ -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(),
} }

View file

@ -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) {

View file

@ -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,10 +20,9 @@ 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
@ -40,8 +38,15 @@ var TODO = `
"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
@ -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
View file

@ -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)

View file

@ -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:

View 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]
}

View file

@ -6,7 +6,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil"
"net" "net"
"time" "time"
@ -40,15 +39,17 @@ 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
outerStore store.BlobStore
grp *stop.Group 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,
outerStore: outer,
grp: stop.New(), grp: stop.New(),
} }
} }
@ -67,9 +68,10 @@ func (s *Server) Start(address string) error {
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))
} }

View file

@ -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)

View file

@ -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
View 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
View 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
View 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
View 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()
}

View file

@ -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

View file

@ -10,17 +10,20 @@ 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"
) )
@ -28,13 +31,15 @@ import (
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{
Handler: r,
Addr: address, Addr: address,
Handler: r,
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()
}

View file

@ -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
View 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()
}

View file

@ -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 {

View file

@ -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
} }
if request.BlobDataPaymentRate != nil {
response.BlobDataPaymentRate = paymentRateAccepted response.BlobDataPaymentRate = paymentRateAccepted
if request.BlobDataPaymentRate < 0 { if *request.BlobDataPaymentRate < 0 {
response.BlobDataPaymentRate = paymentRateTooLow 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"`
} }

View file

@ -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)
}

View file

@ -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
View 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
View 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
View 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")
}

View file

@ -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 {

View file

@ -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()
}

View file

@ -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
}

View file

@ -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() {
} }

View file

@ -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()
}

View file

@ -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"
@ -18,11 +20,12 @@ type DBBackedStore struct {
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()
}

View file

@ -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() {
}

View file

@ -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))
} }

View 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)
}

View 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
View 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() {
}

View file

@ -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++ {
err := lfu.Put(fmt.Sprintf("%d", i), b)
require.NoError(t, err) require.NoError(t, err)
err = lru.Put("two", b) for j := 0; j < 3-i; j++ {
_, _, err = lfu.Get(fmt.Sprintf("%d", i))
require.NoError(t, err) require.NoError(t, err)
err = lru.Put("three", b) }
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
View 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
View 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() {}

View file

@ -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
}

View file

@ -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() {}

View file

@ -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")
} }

View file

@ -1,6 +1,12 @@
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{}
@ -9,7 +15,10 @@ 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 }

View file

@ -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.
@ -113,7 +116,8 @@ func (s *S3Store) Put(hash string, blob stream.Blob) error {
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
}

View file

@ -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
}

View file

@ -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
} }

View file

@ -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.

View file

@ -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)
} }