Showing preview only (3,168K chars total). Download the full file or copy to clipboard to get everything.
Repository: vicanso/pike
Branch: master
Commit: ab016ba37355
Files: 96
Total size: 3.0 MB
Directory structure:
gitextract_52j68_1c/
├── .air.toml
├── .github/
│ └── workflows/
│ ├── build.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── SUMMARY.md
├── app/
│ ├── app.go
│ └── app_test.go
├── asset/
│ └── asset.go
├── cache/
│ ├── cache.go
│ ├── cache_test.go
│ ├── dispatcher.go
│ ├── dispatcher_test.go
│ ├── http_cache.go
│ ├── http_cache_test.go
│ ├── http_response.go
│ ├── http_response_test.go
│ ├── memhash.go
│ └── memhash_test.go
├── compress/
│ ├── brotli.go
│ ├── brotli_test.go
│ ├── compress.go
│ ├── compress_test.go
│ ├── gzip.go
│ ├── gzip_test.go
│ ├── lz4.go
│ ├── lz4_test.go
│ ├── snappy.go
│ ├── snappy_test.go
│ ├── zstd.go
│ └── zstd_test.go
├── config/
│ ├── config.go
│ ├── config_test.go
│ ├── etcd_client.go
│ ├── etcd_client_test.go
│ ├── file_client.go
│ ├── file_client_test.go
│ └── validate.go
├── docs/
│ ├── alarm.md
│ ├── cache-handler.md
│ ├── error.md
│ ├── flow.drawio
│ ├── modules.drawio
│ ├── modules.md
│ ├── performance.md
│ ├── questions.md
│ ├── response.drawio
│ ├── response.md
│ └── start.md
├── entrypoint.sh
├── go.mod
├── go.sum
├── hooks/
│ └── pre-commit
├── location/
│ ├── location.go
│ └── location_test.go
├── log/
│ └── log.go
├── main.go
├── pike.yml
├── schedule/
│ └── schedule.go
├── server/
│ ├── admin.go
│ ├── cache.go
│ ├── cache_test.go
│ ├── proxy.go
│ ├── proxy_test.go
│ ├── responder.go
│ ├── responder_test.go
│ ├── server.go
│ └── server_test.go
├── store/
│ ├── badger.go
│ ├── badger_test.go
│ ├── mongo.go
│ ├── mongo_test.go
│ ├── redis.go
│ ├── redis_test.go
│ ├── store.go
│ └── store_test.go
├── test/
│ └── main.go
├── upstream/
│ ├── upstream.go
│ └── upstream_test.go
├── util/
│ ├── util.go
│ └── util_test.go
└── web/
├── assets/
│ ├── AssetManifest.json
│ ├── FontManifest.json
│ ├── NOTICES
│ ├── fonts/
│ │ └── MaterialIcons-Regular.otf
│ └── packages/
│ └── fluttertoast/
│ └── assets/
│ ├── toastify.css
│ └── toastify.js
├── flutter_service_worker.js
├── index.html
├── main.dart.js
├── manifest.json
└── version.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .air.toml
================================================
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "GO_ENV=dev ./tmp/main --admin :9013 --alarm http://127.0.0.1:3001/alarms"
# Watch these filename extensions.
include_ext = ["go", "yml"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "web"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude unchanged files.
exclude_unchanged = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = true
# Delay after sending Interrupt signal
kill_delay = 500 # ms
[log]
# Show log time
time = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true
================================================
FILE: .github/workflows/build.yml
================================================
name: build on tag
on:
push:
tags:
- 'v*.*.*'
jobs:
docker:
runs-on: ubuntu-latest
name: Build
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Set output
id: vars
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/pike:${{ steps.vars.outputs.tag }}
build-args: GITHUB_SHA=${ GITHUB_SHA },VERSION=${{ steps.vars.outputs.tag }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
================================================
FILE: .github/workflows/test.yml
================================================
name: lint and test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
name: Test
runs-on: ubuntu-latest
services:
etcd:
image: bitnami/etcd
env:
ETCD_ROOT_PASSWORD: 123456
ports:
- 2379:2379
redis:
# Docker Hub image
image: redis
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps port 6379 on service container to the host
- 6379:6379
mongo:
image: mongo
ports:
- 27017:27017
steps:
- name: Build pike
uses: actions/setup-go@v2
with:
go-version: '1.16'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
- name: Lint
run: make lint
- name: Test
run: make test
build:
needs: test
runs-on: ubuntu-latest
name: Build
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/pike:latest
build-args: GITHUB_SHA=${ GITHUB_SHA }
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
================================================
FILE: .gitignore
================================================
vendor
tmp
*.out
*.log
pike
pike-*
packrd
*-packr.go
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
.pnp.js
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
asset/web
dist
================================================
FILE: .goreleaser.yml
================================================
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- make cp-asset
- go mod download
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
================================================
FILE: Dockerfile
================================================
FROM golang:1.16-alpine as builder
COPY ./ /pike
RUN apk update \
&& apk add git make \
&& cd /pike \
&& env \
&& make cp-asset \
&& CGO_ENABLED=0 make build
FROM alpine:3.13
COPY --from=builder /pike/pike /usr/local/bin/pike
COPY --from=builder /pike/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN addgroup -g 1000 pike \
&& adduser -u 1000 -G pike -s /bin/sh -D pike \
&& chmod +x /usr/local/bin/entrypoint.sh \
&& apk add --no-cache ca-certificates
USER pike
WORKDIR /home/pike
CMD ["pike"]
ENTRYPOINT ["entrypoint.sh"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Tree Xie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: default test test-cover dev hooks
# for dev
dev:
air -c .air.toml
# for test
test:
go test -race -cover ./...
test-cover:
go test -race -coverprofile=test.out ./... && go tool cover --html=test.out
bench:
go test -benchmem -bench=. ./...
lint:
golangci-lint run --timeout=2m
tidy:
go mod tidy
cp-asset:
rm -rf asset/web && cp -rf web asset/web
build:
go build -ldflags "-X main.date=`date -u +%Y%m%d.%H%M%S` -X main.commit=`git rev-parse --short HEAD`" -o pike
build-linux:
GOOS=linux GOARCH=amd64 make build && mv pike pike-linux
build-darwin:
GOOS=darwin GOARCH=amd64 make build && mv pike pike-darwin
build-win:
GOOS=windows GOARCH=amd64 make build && mv pike pike-win.exe
hooks:
cp hooks/* .git/hooks/
================================================
FILE: README.md
================================================
# pike
此项目已不再维护,建议使用基于pingora的[pingap](https://github.com/vicanso/pingap)项目。
[](https://github.com/vicanso/pike/actions)
与varnish类似的HTTP缓存服务器,主要的特性如下:
- 提供WEB的管理配置界面,简单易上手
- 支持br与gzip两种压缩方式,根据客户端自动选择。对于可缓存与不可缓存请求使用不同的压缩配置,更佳的时间与空间的平衡
- 仅基于`Cache-Control`生成缓存有效期,接口缓存完全由接口开发者决定,准确而高效(开发比运维更清楚接口是否可缓存,可缓存时长)
- 配置支持文件与etcd两种形式存储,无中断的配置实时更新
- 支持H2C的转发,提升与后端服务的调用性能(如果是内网转发,不需要启用)
- 与upstream的调用支持`gzip`,`brotli`,`lz4`,`snappy`以及`zstd`压缩,可根据与upstream的网络线路选择合适的压缩方式
- 支持upstream检测失败时回调告警,可及时获取异常upstream信息
- 支持自定义日志,可配置按日期与大小分割日志并压缩
- LUR与持久化存储(可选)配合使用,可根据内存使用选择更小的LRU缓存并增加持久化存储的方式
- 持久化存储支持以下形式:badger(文件)、redis以及mongodb
<p align="center">
<img src="./docs/images/home.png"/>
</p>
## 启动方式
启动参数主要如下:
- `config` 配置保存地址,可以指定为etcd或者本地文件,如:`etcd://user:pass@127.0.0.1:2379/pike`,本地文件:`/opt/pike/config.yml`
- `admin` 配置管理后台的访问地址,如:`--admin=:9013`
- `log` 日志文件目录,支持单文件与lumberjack形式,如`/var/pike.log`或`lumberjack:///tmp/pike.log?maxSize=100&maxAge=1&compress=true`,lumberjack会根据文件内容大小与时间将文件分割
### 使用文件保存配置
```bash
# linux etcd,管理后台使用9013端口访问
./pike --config=etcd://127.0.0.1:2379/pike --admin=:9013
# linux file,配置文件保存在/opt/pike.yml,管理后台使用9013端口访问
./pike --config=/opt/pike.yml --admin=:9013
# docker
docker run -it --rm \
-p 9013:9013 \
vicanso/pike:4.0.0-alpha --config=etcd://172.16.183.177:2379/pike --admin=:9013
```
## TODO
- 缓存查询(如果缓存量较大,有可能导致查询性能较差,暂时未支持)
================================================
FILE: SUMMARY.md
================================================
# Summary
* [程序启动](./docs/start.md)
* [模块](./docs/modules.md)
* [缓存处理](./docs/cache-handler.md)
* [响应处理](./docs/response.md)
* [告警](./docs/alarm.md)
* [系统出错](./docs/error.md)
* [性能测试](./docs/performance.md)
* [答疑](./docs/questions.md)
================================================
FILE: app/app.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package app
import (
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/shirou/gopsutil/v3/process"
"github.com/vicanso/pike/log"
"go.uber.org/atomic"
"go.uber.org/zap"
)
type Info struct {
GOARCH string `json:"goarch,omitempty"`
GOOS string `json:"goos,omitempty"`
GoVersion string `json:"goVersion,omitempty"`
Version string `json:"version,omitempty"`
BuildedAt string `json:"buildedAt,omitempty"`
CommitID string `json:"commitID,omitempty"`
Uptime string `json:"uptime,omitempty"`
GoMaxProcs int `json:"goMaxProcs,omitempty"`
CPUUsage int32 `json:"cpuUsage,omitempty"`
RoutineCount int `json:"routineCount,omitempty"`
ThreadCount int32 `json:"threadCount,omitempty"`
RSS uint64 `json:"rss,omitempty"`
RSSHumanize string `json:"rssHumanize,omitempty"`
Swap uint64 `json:"swap,omitempty"`
SwapHumanize string `json:"swapHumanize,omitempty"`
}
var buildedAt time.Time
var commitID string
var version string
var startedAt = time.Now()
var currentProcess *process.Process
var cpuUsage = atomic.NewInt32(-1)
func init() {
p, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
log.Default().Error("new process fail",
zap.Error(err),
)
}
currentProcess = p
_ = UpdateCPUUsage()
}
// SetBuildInfo set build info
func SetBuildInfo(build, id, ver, buildBy string) {
if strings.Contains(build, ".") && len(build) == 15 {
buildedAt, _ = time.Parse("20060102.150405", build)
} else {
buildedAt, _ = time.Parse(time.RFC3339, build)
}
commitID = id
if len(id) > 7 {
commitID = id[0:7]
}
version = ver
}
const MB = 1024 * 1024
func bytesToMB(value uint64) string {
v := value / MB
return strconv.Itoa(int(v)) + " MB"
}
func GetVersion() string {
return version
}
// GetInfo get application info
func GetInfo() *Info {
uptime := ""
d := time.Since(startedAt)
if d > 24*time.Hour {
uptime = strconv.Itoa(int(d/(24*time.Hour))) + "d"
} else if d > time.Hour {
uptime = strconv.Itoa(int(d.Hours())) + "h"
} else {
uptime = (time.Second * time.Duration(d.Seconds())).String()
}
info := &Info{
GOARCH: runtime.GOARCH,
GOOS: runtime.GOOS,
GoVersion: runtime.Version(),
Version: GetVersion(),
BuildedAt: buildedAt.Format(time.RFC3339),
CommitID: commitID,
Uptime: uptime,
GoMaxProcs: runtime.GOMAXPROCS(0),
CPUUsage: cpuUsage.Load(),
RoutineCount: runtime.NumGoroutine(),
}
if currentProcess != nil {
info.ThreadCount, _ = currentProcess.NumThreads()
memInfo, _ := currentProcess.MemoryInfo()
if memInfo != nil {
info.RSS = memInfo.RSS
info.RSSHumanize = bytesToMB(memInfo.RSS)
info.Swap = memInfo.Swap
info.SwapHumanize = bytesToMB(memInfo.Swap)
}
}
return info
}
// UpdateCPUUsage update cpu usage
func UpdateCPUUsage() error {
if currentProcess == nil {
return nil
}
usage, err := currentProcess.Percent(0)
if err != nil {
return err
}
cpuUsage.Store(int32(usage))
return nil
}
================================================
FILE: app/app_test.go
================================================
package app
import (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetSetBuildInfo(t *testing.T) {
assert := assert.New(t)
id := "123"
SetBuildInfo("2021-02-27T01:41:32.416Z", id, "version", "")
assert.Equal(id, commitID)
assert.Equal("2021-02-27 01:41:32.416 +0000 UTC", buildedAt.UTC().String())
}
func TestUpdateCPUUsage(t *testing.T) {
assert := assert.New(t)
err := UpdateCPUUsage()
assert.Nil(err)
}
func TestGetInfo(t *testing.T) {
assert := assert.New(t)
info := GetInfo()
assert.Equal(runtime.GOOS, info.GOOS)
}
================================================
FILE: asset/asset.go
================================================
// MIT License
// Copyright (c) 2021 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package asset
import (
"embed"
)
//go:embed *
var assetFS embed.FS
// GetFS get asset fs
func GetFS() embed.FS {
return assetFS
}
// ReadFile read file data from fs
func ReadFile(file string) ([]byte, error) {
return assetFS.ReadFile(file)
}
================================================
FILE: cache/cache.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package cache
import (
"bytes"
"encoding/binary"
"time"
"unsafe"
"github.com/vicanso/pike/config"
)
// byteSliceToString converts a []byte to string without a heap allocation.
func byteSliceToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// uint32ToBytes convert int to uint32 and convert to bytes
func uint32ToBytes(value int) []byte {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, uint32(value))
return buf
}
// readUint32ToInt read uint32 from bytes and convert to int
func readUint32ToInt(buffer *bytes.Buffer) (int, error) {
var value uint32
err := binary.Read(buffer, binary.BigEndian, &value)
if err != nil {
return 0, err
}
return int(value), nil
}
// uint64ToBytes convert int64 to uint64 and covert to bytes
func uint64ToBytes(value int64) []byte {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(value))
return buf
}
// readUint64 read uint64 from bytes and convert to int64
func readUint64ToInt64(buffer *bytes.Buffer) (int64, error) {
var value uint64
err := binary.Read(buffer, binary.BigEndian, &value)
if err != nil {
return 0, err
}
return int64(value), nil
}
var defaultDispatchers = NewDispatchers(nil)
// GetDispatcher get dispatcher form default dispatchers
func GetDispatcher(name string) *dispatcher {
return defaultDispatchers.Get(name)
}
// RemoveHTTPCache remove http cache form default dispatchers
func RemoveHTTPCache(name string, key []byte) {
defaultDispatchers.RemoveHTTPCache(name, key)
}
func convertConfigs(configs []config.CacheConfig) []DispatcherOption {
opts := make([]DispatcherOption, 0)
for _, item := range configs {
d, _ := time.ParseDuration(item.HitForPass)
opts = append(opts, DispatcherOption{
Name: item.Name,
Size: item.Size,
HitForPass: int(d.Seconds()),
Store: item.Store,
})
}
return opts
}
// ResetDispatchers reset default dispatchers
func ResetDispatchers(configs []config.CacheConfig) {
defaultDispatchers.Reset(convertConfigs(configs))
}
================================================
FILE: cache/cache_test.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package cache
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vicanso/pike/config"
)
func TestConvertConfig(t *testing.T) {
assert := assert.New(t)
name := "cache-test"
size := 100
hitForPass := 60
configs := []config.CacheConfig{
{
Name: name,
Size: size,
HitForPass: "1m",
},
}
opts := convertConfigs(configs)
assert.Equal(1, len(opts))
assert.Equal(name, opts[0].Name)
assert.Equal(size, opts[0].Size)
assert.Equal(hitForPass, opts[0].HitForPass)
}
func TestDefaultDispatcher(t *testing.T) {
assert := assert.New(t)
name := "test"
assert.Nil(GetDispatcher(name))
ResetDispatchers([]config.CacheConfig{
{
Name: name,
Size: 100,
HitForPass: "1m",
},
})
assert.NotNil(GetDispatcher(name))
}
func TestUint32ToBytes(t *testing.T) {
assert := assert.New(t)
buf := uint32ToBytes(1)
value, err := readUint32ToInt(bytes.NewBuffer(buf))
assert.Nil(err)
assert.Equal(1, value)
}
func TestUint64ToBytes(t *testing.T) {
assert := assert.New(t)
buf := uint64ToBytes(1)
value, err := readUint64ToInt64(bytes.NewBuffer(buf))
assert.Nil(err)
assert.Equal(int64(1), value)
}
================================================
FILE: cache/dispatcher.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// 创建缓存分发组件,初始化时创建128长度的lru缓存数组,每次根据缓存的key生成hash,
// 根据hash的值判断使用对应的lru,减少锁的冲突,提升性能
package cache
import (
"sync"
"github.com/golang/groupcache/lru"
"github.com/vicanso/pike/log"
"github.com/vicanso/pike/store"
"github.com/vicanso/pike/util"
"go.uber.org/zap"
)
// defaultZoneSize default zone size
const defaultZoneSize = 128
type (
// httpLRUCache http lru cache
httpLRUCache struct {
cache *lru.Cache
mu *sync.Mutex
}
// dispatcher http cache dispatcher
dispatcher struct {
zoneSize uint64
hitForPass int
list []*httpLRUCache
store store.Store
}
// dispatchers http cache dispatchers
dispatchers struct {
m *sync.Map
}
// DispatcherOption dispatcher option
DispatcherOption struct {
Name string
Size int
HitForPass int
Store string
}
)
func newHTTPLRUCache(size int) *httpLRUCache {
c := &httpLRUCache{
cache: lru.New(size),
mu: &sync.Mutex{},
}
return c
}
// getCache get http cache by key
func (lru *httpLRUCache) getCache(key []byte) (*httpCache, bool) {
value, ok := lru.cache.Get(byteSliceToString(key))
if !ok {
return nil, false
}
if hc, ok := value.(*httpCache); ok {
return hc, true
}
return nil, false
}
// addCache add http cache by key
func (lru *httpLRUCache) addCache(key []byte, hc *httpCache) {
lru.cache.Add(byteSliceToString(key), hc)
}
// removeCache remove http cache by key
func (lru *httpLRUCache) removeCache(key []byte) {
lru.cache.Remove(byteSliceToString(key))
}
// NewDispatcher new a http cache dispatcher
func NewDispatcher(option DispatcherOption) *dispatcher {
zoneSize := defaultZoneSize
size := option.Size
if option.Size <= 0 {
size = zoneSize * 100
}
// 如果配置lru缓存数量较小,则zone的空间调小
if size < 1024 {
zoneSize = 8
}
// 按zoneSize与size创建二维缓存,存放的是LRU缓存实例
lruSize := size / zoneSize
list := make([]*httpLRUCache, zoneSize)
// 根据zone size生成一个缓存对列
for i := 0; i < zoneSize; i++ {
list[i] = newHTTPLRUCache(lruSize)
}
disp := &dispatcher{
zoneSize: uint64(zoneSize),
list: list,
hitForPass: option.HitForPass,
}
// 如果有配置store
if option.Store != "" {
store, err := store.NewStore(option.Store)
if err != nil {
log.Default().Error("new store fail",
zap.String("url", option.Store),
zap.Error(err),
)
}
if store != nil {
disp.store = store
}
}
return disp
}
func (d *dispatcher) getLRU(key []byte) *httpLRUCache {
// 计算hash值
index := MemHash(key) % d.zoneSize
// 从预定义的列表中取对应的缓存
return d.list[index]
}
// GetHTTPCache get http cache through key
func (d *dispatcher) GetHTTPCache(key []byte) *httpCache {
// 锁只在public的方法在使用,public方法之间不互相调用
lru := d.getLRU(key)
lru.mu.Lock()
defer lru.mu.Unlock()
hc, ok := lru.getCache(key)
if ok {
return hc
}
if d.store != nil {
hc = NewHTTPStoreCache(key, d.store)
} else {
hc = NewHTTPCache()
}
lru.addCache(key, hc)
return hc
}
// RemoveHTTPCache remove http cache
func (d *dispatcher) RemoveHTTPCache(key []byte) {
lru := d.getLRU(key)
lru.mu.Lock()
defer lru.mu.Unlock()
lru.removeCache(key)
if d.store != nil {
err := d.store.Delete(key)
if err != nil {
log.Default().Error("delete from store fail",
zap.String("key", string(key)),
zap.Error(err),
)
}
}
}
// GetHitForPass get hit for pass
func (d *dispatcher) GetHitForPass() int {
return d.hitForPass
}
// NewDispatchers new dispatchers
func NewDispatchers(opts []DispatcherOption) *dispatchers {
ds := &dispatchers{
m: &sync.Map{},
}
for _, opt := range opts {
ds.m.Store(opt.Name, NewDispatcher(opt))
}
return ds
}
// Get get dispatcher by name
func (ds *dispatchers) Get(name string) *dispatcher {
value, ok := ds.m.Load(name)
if !ok {
return nil
}
d, ok := value.(*dispatcher)
if !ok {
return nil
}
return d
}
// RemoveHTTPCache remove http cache
func (ds *dispatchers) RemoveHTTPCache(name string, key []byte) {
if name != "" {
d := ds.Get(name)
if d == nil {
return
}
d.RemoveHTTPCache(key)
return
}
// 如果未指定名称,则从所有缓存中删除
ds.m.Range(func(_, v interface{}) bool {
d, ok := v.(*dispatcher)
if ok {
d.RemoveHTTPCache(key)
}
return true
})
}
// Reset reset the dispatchers, remove not exists dispatchers and create new dispatcher. If the dispatcher is exists, then use the old one.
func (ds *dispatchers) Reset(opts []DispatcherOption) {
// 删除不再使用的dispatcher
_ = util.MapDelete(ds.m, func(key string) bool {
// 如果不存在的,则删除
exists := false
for _, opt := range opts {
if opt.Name == key {
exists = true
break
}
}
return !exists
})
for _, opt := range opts {
_, ok := ds.m.Load(opt.Name)
// 如果当前dispatcher不存在,则创建
// 如果存在,对原来的size不调整
if !ok {
ds.m.Store(opt.Name, NewDispatcher(opt))
}
}
}
================================================
FILE: cache/dispatcher_test.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package cache
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLRUGetCache(t *testing.T) {
assert := assert.New(t)
httpLRU := newHTTPLRUCache(10)
key := []byte("abcd")
c, ok := httpLRU.getCache(key)
assert.False(ok)
assert.Nil(c)
httpLRU.cache.Add(string(key), "abc")
c, ok = httpLRU.getCache(key)
assert.False(ok)
assert.Nil(c)
hc := &httpCache{}
httpLRU.cache.Add(string(key), hc)
c, ok = httpLRU.getCache(key)
assert.True(ok)
assert.Equal(hc, c)
}
func TestDispatcher(t *testing.T) {
assert := assert.New(t)
d := NewDispatcher(DispatcherOption{
Size: 0,
HitForPass: 30,
})
assert.Equal(30, d.GetHitForPass())
key := []byte("key")
c := d.GetHTTPCache(key)
c.createdAt = 1
for i := 0; i < 10; i++ {
assert.Equal(c, d.GetHTTPCache([]byte("key")))
}
d.RemoveHTTPCache(key)
hc := d.GetHTTPCache(key)
assert.NotNil(hc)
assert.Empty(hc.createdAt)
}
func TestDispatchers(t *testing.T) {
assert := assert.New(t)
name1 := "test1"
name2 := "test2"
ds := NewDispatchers([]DispatcherOption{
{
Name: name1,
Size: 100,
},
})
assert.NotNil(ds.Get(name1))
// 第一次reset,清除name1,添加name2
ds.Reset([]DispatcherOption{
{
Name: name2,
Size: 100,
},
})
assert.Nil(ds.Get(name1))
assert.NotNil(ds.Get(name2))
// 再次reset
ds.Reset([]DispatcherOption{
{
Name: name2,
Size: 100,
},
})
assert.Nil(ds.Get(name1))
assert.NotNil(ds.Get(name2))
key := []byte("abc")
hc := ds.Get(name2).GetHTTPCache(key)
assert.NotNil(hc)
hc.createdAt = 1
ds.RemoveHTTPCache("", key)
hc1 := ds.Get(name2).GetHTTPCache(key)
assert.Empty(hc1.createdAt)
}
================================================
FILE: cache/http_cache.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// 针对同一个请求,在状态未知时,控制只允许一个请求转发至后续流程
// 在获取状态之后,支持hit for pass 与 hit 两种处理,其中hit for pass表示该请求不可缓存,
// 直接转发至后端程序,而hit则返回当前缓存的响应数据
package cache
import (
"bytes"
"sync"
"time"
"github.com/vicanso/pike/compress"
"github.com/vicanso/pike/log"
"github.com/vicanso/pike/store"
"go.uber.org/zap"
)
type Status int
const (
// StatusUnknown unknown status
StatusUnknown Status = iota
// StatusFetching fetching status
StatusFetching
// StatusHitForPass hit-for-pass status
StatusHitForPass
// StatusHit hit cache status
StatusHit
// StatusPassed pass status
StatusPassed
)
// defaultHitForPassSeconds default hit for pass: 300 seconds
const defaultHitForPassSeconds = 300
type (
// httpCache http cache (only for same request method+host+uri)
httpCache struct {
// key the key of store data
key []byte
// store the store to save http cache
store store.Store
mu *sync.RWMutex
status Status
chanList []chan struct{}
response *HTTPResponse
createdAt int64
expiredAt int64
}
)
func nowUnix() int64 {
return time.Now().Unix()
}
func (i Status) String() string {
switch i {
case StatusFetching:
return "fetching"
case StatusHitForPass:
return "hitForPass"
case StatusHit:
return "hit"
case StatusPassed:
return "passed"
default:
return "unknown"
}
}
// NewHTTPCache new a http cache
func NewHTTPCache() *httpCache {
return &httpCache{
mu: &sync.RWMutex{},
}
}
// NewHTTPStoreCache new a http store cache
func NewHTTPStoreCache(key []byte, store store.Store) *httpCache {
hc := NewHTTPCache()
hc.key = key
hc.store = store
return hc
}
// Get get http cache
func (hc *httpCache) Get() (status Status, response *HTTPResponse) {
hc.mu.Lock()
status, done, response := hc.get()
hc.mu.Unlock()
// 如果done不为空,表示需要等待确认当前请求状态
if done != nil {
// TODO 后续再考虑是否需要添加timeout(proxy部分有超时,因此暂时可不添加)
<-done
// 完成后重新获取当前状态与响应
// 此时状态只可能是hit for pass 或者 hit
// 而此两种状态的数据缓存均不会立即失效,因此可以从hc中获取
status = hc.status
response = hc.response
}
return
}
// Bytes httpcache to bytes
func (hc *httpCache) Bytes() (data []byte, err error) {
statusBuf := uint32ToBytes(int(hc.status))
var respBuf []byte
// 如果有响应数据,则转换
if hc.response != nil {
respBuf, err = hc.response.Bytes()
if err != nil {
return
}
}
respSizeBuf := uint32ToBytes(len(respBuf))
createdAtBuf := uint64ToBytes(hc.createdAt)
expiredAtBuf := uint64ToBytes(hc.expiredAt)
return bytes.Join([][]byte{
statusBuf,
respSizeBuf,
respBuf,
createdAtBuf,
expiredAtBuf,
}, []byte("")), nil
}
// FromBytes restore httpcache from bytes
func (hc *httpCache) FromBytes(data []byte) (err error) {
buffer := bytes.NewBuffer(data)
status, err := readUint32ToInt(buffer)
if err != nil {
return
}
hc.status = Status(status)
respSize, err := readUint32ToInt(buffer)
if err != nil {
return
}
respBuf := buffer.Next(respSize)
resp := &HTTPResponse{}
err = resp.FromBytes(respBuf)
if err != nil {
return
}
hc.response = resp
hc.createdAt, err = readUint64ToInt64(buffer)
if err != nil {
return
}
hc.expiredAt, err = readUint64ToInt64(buffer)
if err != nil {
return
}
return
}
// initFromStore init cache from store
func (hc *httpCache) initFromStore() (err error) {
if hc.store == nil || len(hc.key) == 0 {
return
}
data, err := hc.store.Get(hc.key)
if err != nil {
return
}
return hc.FromBytes(data)
}
// saveToStore save cache to store
func (hc *httpCache) saveToStore() (err error) {
if hc.store == nil || len(hc.key) == 0 {
return
}
data, err := hc.Bytes()
if err != nil {
return
}
ttl := time.Duration(hc.expiredAt-nowUnix()) * time.Second
return hc.store.Set(hc.key, data, ttl)
}
func (hc *httpCache) get() (status Status, done chan struct{}, data *HTTPResponse) {
now := nowUnix()
// 如果首次创建并且设置store
if hc.status == StatusUnknown {
// 如果从缓存中读取失败,暂忽略出错信息
err := hc.initFromStore()
// 如果是无数据,则不输出日志
if err != nil && err != store.ErrNotFound {
log.Default().Error("init from store fail",
zap.Error(err),
)
}
}
// 如果缓存已过期,设置为StatusUnknown
if hc.expiredAt != 0 && hc.expiredAt < now {
hc.status = StatusUnknown
// 将有效期重置(若不重置则导致hs.status每次都被重置为Unknown)
hc.expiredAt = 0
}
// 仅有同类请求为fetching,才会需要等待
// 如果是fetching,则相同的请求需要等待完成
// 通过chan返回完成
if hc.status == StatusFetching {
done = make(chan struct{})
hc.chanList = append(hc.chanList, done)
}
if hc.status == StatusUnknown {
hc.status = StatusFetching
hc.chanList = make([]chan struct{}, 0, 5)
}
status = hc.status
// 为什么需要返回status与data
// 因为有可能在函数调用完成后,刚好缓存过期了,如果此时不返回status与data
// 当其它goroutine获取锁之后,有可能刚好重置数据
if status == StatusHit {
data = hc.response
}
return
}
// HitForPass set the http cache hit for pass
func (hc *httpCache) HitForPass(ttl int) {
hc.mu.Lock()
defer hc.mu.Unlock()
if ttl <= 0 {
ttl = defaultHitForPassSeconds
}
hc.expiredAt = nowUnix() + int64(ttl)
hc.status = StatusHitForPass
list := hc.chanList
hc.chanList = nil
for _, ch := range list {
ch <- struct{}{}
}
err := hc.saveToStore()
if err != nil {
log.Default().Error("save cache to store fail",
zap.String("category", "hitForPass"),
zap.String("key", string(hc.key)),
zap.Error(err),
)
}
}
// Cacheable set http cache cacheable and compress it
func (hc *httpCache) Cacheable(resp *HTTPResponse, ttl int) {
hc.mu.Lock()
defer hc.mu.Unlock()
// 如果是可缓存数据,则选择默认的best compression
resp.CompressSrv = compress.BestCompression
_ = resp.Compress()
hc.createdAt = nowUnix()
hc.expiredAt = hc.createdAt + int64(ttl)
hc.status = StatusHit
hc.response = resp
list := hc.chanList
hc.chanList = nil
for _, ch := range list {
ch <- struct{}{}
}
err := hc.saveToStore()
if err != nil {
log.Default().Error("save cache to store fail",
zap.String("category", "cacheable"),
zap.String("key", string(hc.key)),
zap.Error(err),
)
}
}
// Age get http cache's age
func (hc *httpCache) Age() int {
hc.mu.RLock()
defer hc.mu.RUnlock()
return int(nowUnix() - hc.createdAt)
}
// GetStatus get http cache status
func (hc *httpCache) GetStatus() Status {
hc.mu.RLock()
defer hc.mu.RUnlock()
return hc.status
}
// IsExpired the cache is expired
func (hc *httpCache) IsExpired() bool {
hc.mu.RLock()
defer hc.mu.RUnlock()
if hc.expiredAt == 0 {
return false
}
return hc.expiredAt < nowUnix()
}
================================================
FILE: cache/http_cache_test.go
================================================
package cache
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCacheStatusString(t *testing.T) {
assert := assert.New(t)
assert.Equal("fetching", StatusFetching.String())
assert.Equal("hitForPass", StatusHitForPass.String())
assert.Equal("hit", StatusHit.String())
assert.Equal("passed", StatusPassed.String())
assert.Equal("unknown", StatusUnknown.String())
}
func TestHTTPCacheBytes(t *testing.T) {
assert := assert.New(t)
hc := httpCache{
status: StatusFetching,
response: &HTTPResponse{
CompressSrv: "compress",
},
createdAt: 1,
expiredAt: 2,
}
data, err := hc.Bytes()
assert.Nil(err)
newHC := NewHTTPCache()
err = newHC.FromBytes(data)
assert.Nil(err)
assert.Equal(hc.status, newHC.status)
assert.Equal(hc.response.CompressSrv, newHC.response.CompressSrv)
assert.Equal(hc.createdAt, newHC.createdAt)
assert.Equal(hc.expiredAt, newHC.expiredAt)
}
func TestHTTPCacheGet(t *testing.T) {
assert := assert.New(t)
cacheResp, err := NewHTTPResponse(200, nil, "", []byte("Hello world!"))
// 避免压缩,方便后面对数据检测
cacheResp.CompressMinLength = 1024
assert.Nil(err)
expiredHC := NewHTTPCache()
expiredHC.expiredAt = 1
expiredHC.status = StatusHitForPass
tests := []struct {
status Status
hc *httpCache
resp *HTTPResponse
}{
{
status: StatusHit,
hc: expiredHC,
resp: cacheResp,
},
{
status: StatusHitForPass,
hc: NewHTTPCache(),
},
}
type testResult struct {
status Status
resp *HTTPResponse
}
for _, tt := range tests {
mu := sync.Mutex{}
wg := sync.WaitGroup{}
results := make([]*testResult, 0)
max := 10
for i := 0; i < max; i++ {
wg.Add(1)
go func() {
status, resp := tt.hc.Get()
mu.Lock()
defer mu.Unlock()
results = append(results, &testResult{
status: status,
resp: resp,
})
wg.Done()
}()
}
// 简单等待10ms,让所有for中的goroutine都已执行
time.Sleep(10 * time.Millisecond)
switch tt.status {
case StatusHit:
tt.hc.Cacheable(tt.resp, 300)
case StatusHitForPass:
tt.hc.HitForPass(-1)
}
wg.Wait()
count := 0
for _, result := range results {
// 如果不相等的,只能是fetching
if result.status != tt.status {
assert.Equal(StatusFetching, result.status)
} else {
assert.Equal(tt.status, result.status)
count++
// fetching的数据由fetching取,不使用缓存返回
assert.Equal(tt.resp, result.resp)
}
}
// 其它状态的都相同
assert.Equal(max-1, count)
// 在后续已设置缓存状态之后,再次获取直接返回
status, resp := tt.hc.Get()
assert.Equal(tt.status, status)
assert.Equal(tt.resp, resp)
}
}
func TestHTTPCacheAge(t *testing.T) {
assert := assert.New(t)
hc := httpCache{
createdAt: nowUnix() - 1,
mu: &sync.RWMutex{},
}
assert.GreaterOrEqual(hc.Age(), 1)
}
func TestHTTPCacheGetStatus(t *testing.T) {
assert := assert.New(t)
hc := httpCache{
status: StatusFetching,
mu: &sync.RWMutex{},
}
assert.Equal(StatusFetching, hc.GetStatus())
}
func TestHTTPCacheIsExpired(t *testing.T) {
assert := assert.New(t)
hc := httpCache{
expiredAt: 0,
mu: &sync.RWMutex{},
}
assert.False(hc.IsExpired())
hc.expiredAt = 1
assert.True(hc.IsExpired())
hc.expiredAt = nowUnix() + 10
assert.False(hc.IsExpired())
}
================================================
FILE: cache/http_response.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// HTTP响应数据,只用于根据客户端支持编码以及最小压缩长度返回对应的数据
// 对于可缓存且大于最小缓存则可使用compress方法保存gzip与br两种缓存数据,
// 在客户端请求时根据客户端支持的编码返回,若不支持压缩,则从解压获取原始数据返回
// 对于不可缓存数据,根据客户端支持的编码以及数据长度返回对应数据
package cache
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"regexp"
"strings"
"github.com/vicanso/elton"
"github.com/vicanso/pike/compress"
)
var ignoreHeaders = []string{
"Content-Encoding",
"Content-Length",
"Connection",
"Date",
}
var ErrBodyIsNil = errors.New("body is nil")
var defaultCompressContentTypeFilter = regexp.MustCompile(`text|javascript|json|wasm|xml|font`)
type (
// HTTPResponse http response's cache
HTTPResponse struct {
// 压缩服务名称
CompressSrv string `json:"compressSrv,omitempty"`
// 压缩最小尺寸
CompressMinLength int `json:"compressMinLength,omitempty"`
// 压缩数据类型
CompressContentTypeFilter *regexp.Regexp `json:"-"`
// 响应头
Header http.Header `json:"header,omitempty"`
// 响应状态码
StatusCode int `json:"statusCode,omitempty"`
GzipBody []byte `json:"gzipBody,omitempty"`
BrBody []byte `json:"brBody,omitempty"`
RawBody []byte `json:"rawBody,omitempty"`
}
)
func cloneHeaderAndIgnore(header http.Header) http.Header {
h := header.Clone()
for _, key := range ignoreHeaders {
h.Del(key)
}
return h
}
// NewHTTPResponse new a http response
func NewHTTPResponse(statusCode int, header http.Header, encoding string, data []byte) (*HTTPResponse, error) {
resp := &HTTPResponse{
StatusCode: statusCode,
Header: cloneHeaderAndIgnore(header),
}
switch encoding {
case compress.EncodingGzip:
resp.GzipBody = data
case compress.EncodingBrotli:
resp.BrBody = data
case "":
resp.RawBody = data
default:
// 取默认的compress来解压
compressSrv := compress.Get("")
data, err := compressSrv.Decompress(encoding, data)
if err != nil {
return nil, err
}
header.Del(elton.HeaderContentEncoding)
resp.RawBody = data
}
return resp, nil
}
// Bytes http response to bytes
func (resp *HTTPResponse) Bytes() (data []byte, err error) {
var contentTypeFilter string
if resp.CompressContentTypeFilter != nil {
contentTypeFilter = resp.CompressContentTypeFilter.String()
}
// 压缩服务名称
compressSrvBuf := []byte(resp.CompressSrv)
compressSrvBufSize := uint32ToBytes(len(compressSrvBuf))
// 最小压缩尺寸,4个字节
compressMinLengthBuf := uint32ToBytes(resp.CompressMinLength)
// // 压缩类型,4个字节保存长度
filterBuf := []byte(contentTypeFilter)
filterBufSize := uint32ToBytes(len(filterBuf))
// 响应头,4个字节保存长度
headerBuf, err := json.Marshal(resp.Header)
if err != nil {
return
}
headerBufSize := uint32ToBytes(len(headerBuf))
// 响应码,4个字节
statusCodeBuf := uint32ToBytes(resp.StatusCode)
// gzip,4个字节保存长度
gzipBufSize := uint32ToBytes(len(resp.GzipBody))
// br,4个字节保存长度
brBufSize := uint32ToBytes(len(resp.BrBody))
// raw,4个字节保存长度
rawBufSize := uint32ToBytes(len(resp.RawBody))
return bytes.Join([][]byte{
compressSrvBufSize,
compressSrvBuf,
compressMinLengthBuf,
filterBufSize,
filterBuf,
headerBufSize,
headerBuf,
statusCodeBuf,
gzipBufSize,
resp.GzipBody,
brBufSize,
resp.BrBody,
rawBufSize,
resp.RawBody,
}, []byte("")), nil
}
// FromBytes http response from bytes
func (resp *HTTPResponse) FromBytes(data []byte) (err error) {
if len(data) == 0 {
return
}
buffer := bytes.NewBuffer(data)
size, err := readUint32ToInt(buffer)
if err != nil {
return
}
resp.CompressSrv = string(buffer.Next(size))
resp.CompressMinLength, err = readUint32ToInt(buffer)
if err != nil {
return
}
size, err = readUint32ToInt(buffer)
if err != nil {
return
}
contentTypeFilter := string(buffer.Next(size))
if contentTypeFilter != "" {
resp.CompressContentTypeFilter, err = regexp.Compile(contentTypeFilter)
if err != nil {
return
}
}
size, err = readUint32ToInt(buffer)
if err != nil {
return
}
headerBuf := buffer.Next(size)
err = json.Unmarshal(headerBuf, &resp.Header)
if err != nil {
return
}
resp.StatusCode, err = readUint32ToInt(buffer)
if err != nil {
return
}
size, err = readUint32ToInt(buffer)
if err != nil {
return
}
resp.GzipBody = buffer.Next(size)
size, err = readUint32ToInt(buffer)
if err != nil {
return
}
resp.BrBody = buffer.Next(size)
size, err = readUint32ToInt(buffer)
if err != nil {
return
}
resp.RawBody = buffer.Next(size)
return
}
func (resp *HTTPResponse) shouldCompressed() bool {
// 如果数据都小于最小压缩长度,则表示无需压缩
if len(resp.RawBody) <= resp.CompressMinLength &&
len(resp.GzipBody) <= resp.CompressMinLength &&
len(resp.BrBody) <= resp.CompressMinLength {
return false
}
filter := resp.CompressContentTypeFilter
if filter == nil {
filter = defaultCompressContentTypeFilter
}
// 数据类型匹配才可压缩
return filter.MatchString(resp.Header.Get(elton.HeaderContentType))
}
// GetRawBody get raw body of http response(not compress)
func (resp *HTTPResponse) GetRawBody() (rawBody []byte, err error) {
rawBody = resp.RawBody
if len(rawBody) != 0 {
return
}
compressSrv := compress.Get("")
// 原始数据为空,需要从gzip或br中解压
if len(resp.GzipBody) != 0 {
return compressSrv.Gunzip(resp.GzipBody)
}
if len(resp.BrBody) != 0 {
return compressSrv.BrotliDecode(resp.BrBody)
}
return
}
// Compress compress http response's data
func (resp *HTTPResponse) Compress() (err error) {
// 如果数据不需要压缩,则直接返回
if !resp.shouldCompressed() {
return
}
// 如果gzip与br均已压缩
if len(resp.GzipBody) != 0 && len(resp.BrBody) != 0 {
return
}
rawBody, err := resp.GetRawBody()
if err != nil {
return
}
// 如果原始数据为空,则直接报错,因为如果数据为空,则在前置判断是否可压缩已返回
if len(rawBody) == 0 {
err = ErrBodyIsNil
return
}
compressSrv := compress.Get(resp.CompressSrv)
if len(resp.GzipBody) == 0 {
resp.GzipBody, err = compressSrv.Gzip(rawBody)
if err != nil {
return
}
}
if len(resp.BrBody) == 0 {
resp.BrBody, err = compressSrv.Brotli(rawBody)
if err != nil {
return
}
}
// 压缩后清空原始数据,因为基本所有的客户端都支持gzip,
// 没必要再保存原始数据,如果有需要,可以从gzip中解压
resp.RawBody = nil
return
}
func (resp *HTTPResponse) getBodyByAcceptEncoding(acceptEncoding string) (encoding string, body []byte, err error) {
compressSrv := compress.Get(resp.CompressSrv)
// 如果支持br,而且br有数据
acceptBr := strings.Contains(acceptEncoding, compress.EncodingBrotli)
if acceptBr && len(resp.BrBody) != 0 {
return compress.EncodingBrotli, resp.BrBody, nil
}
// 如果支持gzip,而且gzip有数据
acceptGzip := strings.Contains(acceptEncoding, compress.EncodingGzip)
if acceptGzip && len(resp.GzipBody) != 0 {
return compress.EncodingGzip, resp.GzipBody, nil
}
// 获取原始数据压缩
rawBody, err := resp.GetRawBody()
if err != nil {
return "", nil, err
}
shouldCompressed := resp.shouldCompressed()
// 数据不应该压缩,直接返回
if !shouldCompressed {
return "", rawBody, nil
}
// 支持br,数据从原始数据压缩
if acceptBr {
brBody, err := compressSrv.Brotli(rawBody)
if err != nil {
return "", nil, err
}
return compress.EncodingBrotli, brBody, nil
}
// 支持gzip,数据从原始数据压缩
if acceptGzip {
gzipBody, err := compressSrv.Gzip(rawBody)
if err != nil {
return "", nil, err
}
return compress.EncodingGzip, gzipBody, nil
}
// 都不支持,返回原始数据
return "", rawBody, nil
}
// Fill fill response to context
func (resp *HTTPResponse) Fill(c *elton.Context) (err error) {
encoding, body, err := resp.getBodyByAcceptEncoding(c.GetRequestHeader(elton.HeaderAcceptEncoding))
if err != nil {
return
}
c.MergeHeader(resp.Header)
c.SetHeader(elton.HeaderContentEncoding, encoding)
c.StatusCode = resp.StatusCode
c.BodyBuffer = bytes.NewBuffer(body)
return
}
================================================
FILE: cache/http_response_test.go
================================================
package cache
import (
"encoding/json"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"github.com/golang/snappy"
"github.com/stretchr/testify/assert"
"github.com/vicanso/elton"
"github.com/vicanso/pike/compress"
)
func TestCloneHeaderAndIgnore(t *testing.T) {
assert := assert.New(t)
assert.Equal("Content-Encoding,Content-Length,Connection,Date", strings.Join(ignoreHeaders, ","))
h := make(http.Header)
for _, key := range ignoreHeaders {
h.Add(key, "value")
}
newHeader := cloneHeaderAndIgnore(h)
for _, key := range ignoreHeaders {
assert.Empty(newHeader.Get(key))
}
}
func TestHTTPResponseMarshal(t *testing.T) {
assert := assert.New(t)
header := make(http.Header)
header.Add("a", "1")
header.Add("a", "2")
header.Add("b", "3")
resp := &HTTPResponse{
CompressSrv: "compress",
CompressMinLength: 1000,
CompressContentTypeFilter: regexp.MustCompile(`a|b|c`),
Header: header,
StatusCode: 200,
GzipBody: []byte("gzip"),
BrBody: []byte("br"),
RawBody: []byte("raw"),
}
data, err := resp.Bytes()
assert.Nil(err)
newResp := &HTTPResponse{}
err = newResp.FromBytes(data)
assert.Nil(err)
assert.Equal(resp.CompressSrv, newResp.CompressSrv)
assert.Equal(resp.CompressMinLength, newResp.CompressMinLength)
assert.Equal(resp.CompressContentTypeFilter, newResp.CompressContentTypeFilter)
assert.Equal(resp.Header, newResp.Header)
assert.Equal(resp.StatusCode, newResp.StatusCode)
assert.Equal(resp.GzipBody, newResp.GzipBody)
assert.Equal(resp.BrBody, newResp.BrBody)
assert.Equal(resp.RawBody, newResp.RawBody)
}
func TestNewHTTPResponse(t *testing.T) {
assert := assert.New(t)
data := []byte("Hello world!")
compressSrv := compress.Get("")
tests := []struct {
statusCode int
header http.Header
encoding string
fn func() ([]byte, error)
}{
{
statusCode: 200,
header: http.Header{},
encoding: compress.EncodingGzip,
fn: func() ([]byte, error) {
return compressSrv.Gzip(data)
},
},
{
statusCode: 201,
header: http.Header{},
encoding: compress.EncodingBrotli,
fn: func() ([]byte, error) {
return compressSrv.Brotli(data)
},
},
{
statusCode: 200,
header: http.Header{},
encoding: compress.EncodingSnappy,
fn: func() ([]byte, error) {
dst := []byte{}
dst = snappy.Encode(dst, data)
return dst, nil
},
},
}
for _, tt := range tests {
result, err := tt.fn()
assert.Nil(err)
resp, err := NewHTTPResponse(tt.statusCode, tt.header, tt.encoding, result)
assert.Nil(err)
assert.Equal(tt.statusCode, resp.StatusCode)
switch tt.encoding {
case compress.EncodingGzip:
assert.NotNil(resp.GzipBody)
assert.Nil(resp.RawBody)
assert.Nil(resp.BrBody)
case compress.EncodingBrotli:
assert.NotNil(resp.BrBody)
assert.Nil(resp.GzipBody)
assert.Nil(resp.RawBody)
default:
assert.NotNil(resp.RawBody)
assert.Nil(resp.GzipBody)
assert.Nil(resp.BrBody)
}
}
}
func TestShouldCompressed(t *testing.T) {
assert := assert.New(t)
data := []byte("Hello world!")
tests := []struct {
header http.Header
rawBody []byte
gzipBody []byte
brBody []byte
shouldCompressed bool
}{
{
shouldCompressed: false,
},
{
header: http.Header{
elton.HeaderContentType: []string{"image/png"},
},
rawBody: data,
shouldCompressed: false,
},
{
header: http.Header{
elton.HeaderContentType: []string{"application/json"},
},
rawBody: data,
shouldCompressed: true,
},
{
header: http.Header{
elton.HeaderContentType: []string{"application/json"},
},
gzipBody: data,
shouldCompressed: true,
},
{
header: http.Header{
elton.HeaderContentType: []string{"application/json"},
},
brBody: data,
shouldCompressed: true,
},
}
for _, tt := range tests {
resp := &HTTPResponse{
Header: tt.header,
RawBody: tt.rawBody,
GzipBody: tt.gzipBody,
BrBody: tt.brBody,
CompressMinLength: 1,
}
result := resp.shouldCompressed()
assert.Equal(tt.shouldCompressed, result)
}
}
func TestGetRawBody(t *testing.T) {
assert := assert.New(t)
compressSrv := compress.Get("")
data := []byte("Hello world!")
gzipData, err := compressSrv.Gzip(data)
assert.Nil(err)
brData, err := compressSrv.Brotli(data)
assert.Nil(err)
tests := []struct {
rawBody []byte
gzipBody []byte
brBody []byte
}{
{
rawBody: data,
},
{
gzipBody: gzipData,
},
{
brBody: brData,
},
}
for _, tt := range tests {
resp := &HTTPResponse{
RawBody: tt.rawBody,
BrBody: tt.brBody,
GzipBody: tt.gzipBody,
}
rawBody, err := resp.GetRawBody()
assert.Nil(err)
assert.Equal(data, rawBody)
}
}
func TestCompress(t *testing.T) {
assert := assert.New(t)
data := []byte("Hello world!")
compressSrv := compress.Get("")
gzipData, err := compressSrv.Gzip(data)
assert.Nil(err)
brData, err := compressSrv.Brotli(data)
assert.Nil(err)
tests := []struct {
rawBody []byte
gzipBody []byte
brBody []byte
}{
{
rawBody: data,
},
{
gzipBody: gzipData,
brBody: brData,
},
}
for _, tt := range tests {
resp := &HTTPResponse{
Header: http.Header{
elton.HeaderContentType: []string{"application/json"},
},
RawBody: tt.rawBody,
GzipBody: tt.gzipBody,
BrBody: tt.brBody,
CompressMinLength: 1,
}
err := resp.Compress()
assert.Nil(err)
assert.Nil(resp.RawBody)
assert.Equal(gzipData, resp.GzipBody)
assert.Equal(brData, resp.BrBody)
}
}
func TestGetBodyByAcceptEncoding(t *testing.T) {
assert := assert.New(t)
data := []byte("Hello world!")
compressSrv := compress.Get("")
gzipData, err := compressSrv.Gzip(data)
assert.Nil(err)
brData, err := compressSrv.Brotli(data)
assert.Nil(err)
tests := []struct {
rawBody []byte
gzipBody []byte
brBody []byte
acceptEncoding string
minLength int
resultEncoding string
result []byte
}{
// 支持br且已存在br
{
brBody: brData,
acceptEncoding: compress.EncodingBrotli,
resultEncoding: compress.EncodingBrotli,
result: brData,
},
// 支持gzip且已存在gzip
{
gzipBody: gzipData,
acceptEncoding: compress.EncodingGzip,
resultEncoding: compress.EncodingGzip,
result: gzipData,
},
// 数据不应该被压缩,没有gzip,而且原始数据小于最小压缩长度
{
rawBody: data,
minLength: 1000,
acceptEncoding: compress.EncodingGzip,
resultEncoding: "",
result: data,
},
// 支持br但没有br,且数据不应该压缩
{
rawBody: data,
minLength: 1000,
acceptEncoding: compress.EncodingBrotli,
resultEncoding: "",
result: data,
},
// 支持br,而且原始数据大于最小压缩长度,压缩后返回
{
rawBody: data,
acceptEncoding: compress.EncodingBrotli,
resultEncoding: compress.EncodingBrotli,
result: brData,
},
// 支持gzip,而且压缩数据大于最小压缩长度,压缩后返回
{
rawBody: data,
acceptEncoding: compress.EncodingGzip,
resultEncoding: compress.EncodingGzip,
result: gzipData,
},
// 不支持压缩,从br中返回
{
brBody: brData,
acceptEncoding: "",
resultEncoding: "",
result: data,
},
// 不支持压缩,从gzip中返回
{
gzipBody: gzipData,
acceptEncoding: "",
resultEncoding: "",
result: data,
},
}
for _, tt := range tests {
resp := &HTTPResponse{
Header: http.Header{
elton.HeaderContentType: []string{"application/json"},
},
RawBody: tt.rawBody,
GzipBody: tt.gzipBody,
BrBody: tt.brBody,
CompressMinLength: tt.minLength,
}
encoding, body, err := resp.getBodyByAcceptEncoding(tt.acceptEncoding)
assert.Nil(err)
assert.Equal(tt.resultEncoding, encoding)
assert.Equal(tt.result, body)
}
}
func TestFill(t *testing.T) {
assert := assert.New(t)
data := []byte("Hello world!")
compressSrv := compress.Get("")
gzipData, err := compressSrv.Gzip(data)
assert.Nil(err)
brData, err := compressSrv.Brotli(data)
assert.Nil(err)
tests := []struct {
rawBody []byte
acceptEncoding string
resultEncoding string
result []byte
}{
{
rawBody: data,
acceptEncoding: compress.EncodingBrotli,
resultEncoding: compress.EncodingBrotli,
result: brData,
},
{
rawBody: data,
acceptEncoding: compress.EncodingGzip,
resultEncoding: compress.EncodingGzip,
result: gzipData,
},
{
rawBody: data,
acceptEncoding: "",
resultEncoding: "",
result: data,
},
}
for _, tt := range tests {
resp := &HTTPResponse{
Header: http.Header{
elton.HeaderContentType: []string{"application/json"},
},
RawBody: tt.rawBody,
StatusCode: 200,
}
req := httptest.NewRequest("GET", "/", nil)
c := elton.NewContext(httptest.NewRecorder(), req)
c.SetRequestHeader(elton.HeaderAcceptEncoding, tt.acceptEncoding)
err := resp.Fill(c)
assert.Nil(err)
assert.Equal(200, c.StatusCode)
assert.Equal(tt.resultEncoding, c.GetHeader(elton.HeaderContentEncoding))
assert.Equal(tt.result, c.BodyBuffer.Bytes())
}
}
func BenchmarkMarshalHTTPResponse(b *testing.B) {
resp := &HTTPResponse{
CompressSrv: "compress",
CompressMinLength: 1024,
Header: http.Header{
"Content-Type": []string{
"application/json; charset=UTF-8",
},
"ETag": []string{
`"60-R79m3yeTgBMQWG5Ysx2j_T3gIsM="`,
},
},
GzipBody: make([]byte, 5*1024),
BrBody: make([]byte, 5*1024),
}
for i := 0; i < b.N; i++ {
buf, err := json.Marshal(resp)
if err != nil || len(buf) == 0 {
panic("to bytes fail")
}
}
}
func BenchmarkHTTPResponseToBytes(b *testing.B) {
resp := &HTTPResponse{
CompressSrv: "compress",
CompressMinLength: 1024,
CompressContentTypeFilter: regexp.MustCompile(`text|javascript|json|wasm|xml|font`),
Header: http.Header{
"Content-Type": []string{
"application/json; charset=UTF-8",
},
"ETag": []string{
`"60-R79m3yeTgBMQWG5Ysx2j_T3gIsM="`,
},
},
GzipBody: make([]byte, 5*1024),
BrBody: make([]byte, 5*1024),
}
for i := 0; i < b.N; i++ {
buf, err := resp.Bytes()
if err != nil || len(buf) == 0 {
panic("to bytes fail")
}
}
}
================================================
FILE: cache/memhash.go
================================================
// copy from https://github.com/dgraph-io/ristretto/blob/master/z/rtutil.go
package cache
import (
"unsafe"
)
// NanoTime returns the current time in nanoseconds from a monotonic clock.
//go:linkname NanoTime runtime.nanotime
func NanoTime() int64
// CPUTicks is a faster alternative to NanoTime to measure time duration.
//go:linkname CPUTicks runtime.cputicks
func CPUTicks() int64
type stringStruct struct {
str unsafe.Pointer
len int
}
//go:noescape
//go:linkname memhash runtime.memhash
func memhash(p unsafe.Pointer, h, s uintptr) uintptr
// MemHash is the hash function used by go map, it utilizes available hardware instructions(behaves
// as aeshash if aes instruction is available).
// NOTE: The hash seed changes for every process. So, this cannot be used as a persistent hash.
func MemHash(data []byte) uint64 {
ss := (*stringStruct)(unsafe.Pointer(&data))
return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}
// MemHashString is the hash function used by go map, it utilizes available hardware instructions
// (behaves as aeshash if aes instruction is available).
// NOTE: The hash seed changes for every process. So, this cannot be used as a persistent hash.
func MemHashString(str string) uint64 {
ss := (*stringStruct)(unsafe.Pointer(&str))
return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}
// FastRand is a fast thread local random function.
//go:linkname FastRand runtime.fastrand
func FastRand() uint32
================================================
FILE: cache/memhash_test.go
================================================
package cache
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMemHash(t *testing.T) {
assert := assert.New(t)
data := "GET aslant.site /users/v1/me"
v := MemHash([]byte(data))
assert.Equal(v, MemHash([]byte(data)))
assert.Equal(v, MemHashString(data))
assert.NotEqual(v, MemHash([]byte("abc")))
}
================================================
FILE: compress/brotli.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package compress
import (
"bytes"
"io/ioutil"
"github.com/andybalholm/brotli"
)
const (
defaultBrQuality = 6
)
func brotliEncode(buf []byte, level int) (*bytes.Buffer, error) {
buffer := new(bytes.Buffer)
if level <= 0 || level > 11 {
level = defaultBrQuality
}
w := brotli.NewWriterLevel(buffer, level)
defer w.Close()
_, err := w.Write(buf)
if err != nil {
return nil, err
}
return buffer, nil
}
// doBrotli brotli compress
func doBrotli(buf []byte, level int) ([]byte, error) {
buffer, err := brotliEncode(buf, level)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// doBrotliDecode brotli decode
func doBrotliDecode(buf []byte) ([]byte, error) {
if len(buf) == 0 {
return nil, nil
}
r := brotli.NewReader(bytes.NewBuffer(buf))
return ioutil.ReadAll(r)
}
================================================
FILE: compress/brotli_test.go
================================================
package compress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDoBrotli(t *testing.T) {
assert := assert.New(t)
// 压缩级别超出则与默认压缩级别一样
tests := []struct {
data []byte
level int
resultSize int
}{
{
data: compressTestData,
level: 0,
resultSize: 589,
},
{
data: compressTestData,
level: 12,
resultSize: 589,
},
{
data: compressTestData,
level: 8,
resultSize: 592,
},
}
for _, tt := range tests {
data, err := doBrotli(tt.data, tt.level)
assert.Nil(err)
assert.Equal(tt.resultSize, len(data))
assert.NotEqual(tt.data, data)
}
}
func TestDoBrotliDecode(t *testing.T) {
assert := assert.New(t)
tests := []struct {
data []byte
}{
{
data: compressTestData,
},
}
for _, tt := range tests {
data, err := doBrotli(tt.data, 0)
assert.Nil(err)
assert.NotNil(data)
assert.NotEqual(tt.data, data)
data, err = doBrotliDecode(data)
assert.Nil(err)
assert.Equal(tt.data, data)
}
}
================================================
FILE: compress/compress.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package compress
import (
"compress/gzip"
"errors"
"sync"
"github.com/andybalholm/brotli"
"github.com/vicanso/pike/config"
"go.uber.org/atomic"
)
const (
EncodingGzip = "gzip"
EncodingBrotli = "br"
EncodingLZ4 = "lz4"
EncodingSnappy = "snz"
EncodingZSTD = "zst"
)
type (
compressSrv struct {
levels map[string]*atomic.Int32
}
CompressOption struct {
Name string
Levels map[string]int
}
compressSrvs struct {
m *sync.Map
}
)
const BestCompression = "bestCompression"
var defaultCompressSrvList = NewServices([]CompressOption{
{
Name: BestCompression,
Levels: map[string]int{
// -1则会选择默认的压缩级别
"br": -1,
"gzip": gzip.BestCompression,
},
},
})
var defaultCompressSrv = NewService()
var notSupportedEncoding = errors.New("not supported encoding")
// NewServices new compress services
func NewServices(opts []CompressOption) *compressSrvs {
cs := &compressSrvs{
m: &sync.Map{},
}
for _, opt := range opts {
srv := NewService()
srv.SetLevels(opt.Levels)
cs.m.Store(opt.Name, srv)
}
return cs
}
// NewService new compress service
func NewService() *compressSrv {
// 配置压缩级别,只设置了gzip与br
levels := map[string]*atomic.Int32{
EncodingGzip: atomic.NewInt32(gzip.DefaultCompression),
EncodingBrotli: atomic.NewInt32(brotli.DefaultCompression),
}
return &compressSrv{
levels: levels,
}
}
// Get get service by name
func (cs *compressSrvs) Get(name string) *compressSrv {
value, ok := cs.m.Load(name)
if !ok {
return defaultCompressSrv
}
srv, ok := value.(*compressSrv)
if !ok {
return defaultCompressSrv
}
return srv
}
// Reset reset the services
func (cs *compressSrvs) Reset(opts []CompressOption) {
// 此处不删除存在的压缩服务,因为compress实例并不占多少内存
// 也避免配置了bestCompression后删除
for _, opt := range opts {
srv := NewService()
srv.SetLevels(opt.Levels)
cs.m.Store(opt.Name, srv)
}
}
func convertConfigs(configs []config.CompressConfig) []CompressOption {
opts := make([]CompressOption, 0)
for _, item := range configs {
levels := make(map[string]int)
for key, value := range item.Levels {
levels[key] = int(value)
}
opts = append(opts, CompressOption{
Name: item.Name,
Levels: levels,
})
}
return opts
}
// Reset reset default compress services
func Reset(configs []config.CompressConfig) {
defaultCompressSrvList.Reset(convertConfigs(configs))
}
// Get get default compress service
func Get(name string) *compressSrv {
return defaultCompressSrvList.Get(name)
}
// GetLevel get compress level
func (srv *compressSrv) GetLevel(encoding string) int {
levelValue, ok := srv.levels[encoding]
if !ok {
return 0
}
return int(levelValue.Load())
}
// SetLevels set compres levels
func (srv *compressSrv) SetLevels(levels map[string]int) {
for name, value := range levels {
levelValue, ok := srv.levels[name]
if ok {
levelValue.Store(int32(value))
}
}
}
// Decompress decompress data
func (srv *compressSrv) Decompress(encoding string, data []byte) ([]byte, error) {
switch encoding {
case EncodingGzip:
return srv.Gunzip(data)
case EncodingBrotli:
return srv.BrotliDecode(data)
case EncodingLZ4:
return srv.LZ4Decode(data)
case EncodingSnappy:
return srv.SnappyDecode(data)
case EncodingZSTD:
return srv.ZSTDDecode(data)
case "":
return data, nil
}
return nil, notSupportedEncoding
}
// Gzip compress data by gzip
func (srv *compressSrv) Gzip(data []byte) ([]byte, error) {
level := srv.GetLevel(EncodingGzip)
return doGzip(data, level)
}
// Gunzip decompress data by gzip
func (srv *compressSrv) Gunzip(data []byte) ([]byte, error) {
return doGunzip(data)
}
// Brotli compress data by br
func (srv *compressSrv) Brotli(data []byte) ([]byte, error) {
level := srv.GetLevel(EncodingBrotli)
return doBrotli(data, level)
}
// BrotliDecode decompress data by brotli
func (srv *compressSrv) BrotliDecode(data []byte) ([]byte, error) {
return doBrotliDecode(data)
}
// LZ4Decode decompress data by lz4
func (srv *compressSrv) LZ4Decode(data []byte) ([]byte, error) {
return doLZ4Decode(data)
}
// SnappyDecode decompress data by snappy
func (srv *compressSrv) SnappyDecode(data []byte) ([]byte, error) {
return doSnappyDecode(data)
}
// ZSTDDecode decompress data by zstd
func (srv *compressSrv) ZSTDDecode(data []byte) ([]byte, error) {
return doZSTDDecode(data)
}
================================================
FILE: compress/compress_test.go
================================================
package compress
import (
"compress/gzip"
"testing"
"github.com/andybalholm/brotli"
"github.com/stretchr/testify/assert"
"github.com/vicanso/pike/config"
)
var compressTestData = []byte(`Brotli is a data format specification[2] for data streams compressed with a specific combination of the general-purpose LZ77 lossless compression algorithm, Huffman coding and 2nd order context modelling. Brotli is a compression algorithm developed by Google and works best for text compression.
Google employees Jyrki Alakuijala and Zoltán Szabadka initially developed Brotli to decrease the size of transmissions of WOFF2 web fonts, and in that context Brotli was a continuation of the development of zopfli, which is a zlib-compatible implementation of the standard gzip and deflate specifications. Brotli allows a denser packing than gzip and deflate because of several algorithmic and format level improvements: the use of context models for literals and copy distances, describing copy distances through past distances, use of move-to-front queue in entropy code selection, joint-entropy coding of literal and copy lengths, the use of graph algorithms in block splitting, and a larger backward reference window are example improvements. The Brotli specification was generalized in September 2015 for HTTP stream compression (content-encoding type 'br'). This generalized iteration also improved the compression ratio by using a pre-defined dictionary of frequently used words and phrases.`)
func TestConvertConfig(t *testing.T) {
assert := assert.New(t)
name := "compress-test"
levels := map[string]uint{
"gzip": 9,
"br": 8,
}
configs := []config.CompressConfig{
{
Name: name,
Levels: levels,
},
}
opts := convertConfigs(configs)
assert.Equal(1, len(opts))
assert.Equal(name, opts[0].Name)
assert.Equal(9, opts[0].Levels["gzip"])
assert.Equal(8, opts[0].Levels["br"])
}
func TestCompressLevel(t *testing.T) {
assert := assert.New(t)
srv := NewService()
assert.Equal(gzip.DefaultCompression, srv.GetLevel(EncodingGzip))
assert.Equal(brotli.DefaultCompression, srv.GetLevel(EncodingBrotli))
srv.SetLevels(map[string]int{
EncodingGzip: 1,
EncodingBrotli: 2,
})
assert.Equal(1, srv.GetLevel(EncodingGzip))
assert.Equal(2, srv.GetLevel(EncodingBrotli))
}
func TestCompressList(t *testing.T) {
assert := assert.New(t)
srvList := NewServices([]CompressOption{
{
Name: "test",
Levels: map[string]int{
"gzip": 1,
"br": 2,
},
},
})
srv := srvList.Get("test")
assert.Equal(1, srv.GetLevel("gzip"))
assert.Equal(2, srv.GetLevel("br"))
srvList.Reset([]CompressOption{
{
Name: "test1",
Levels: map[string]int{
"gzip": 3,
"br": 4,
},
},
})
// compress并不删除原有的srv
assert.Equal(srv, srvList.Get("test"))
srv = srvList.Get("test1")
assert.Equal(3, srv.GetLevel("gzip"))
assert.Equal(4, srv.GetLevel("br"))
// 从默认获取
assert.Equal(defaultCompressSrv, Get("test"))
Reset([]config.CompressConfig{
{
Name: "test",
Levels: map[string]uint{
"gzip": 3,
"br": 4,
},
},
})
assert.Equal(3, Get("test").GetLevel("gzip"))
assert.Equal(4, Get("test").GetLevel("br"))
}
func TestDecompress(t *testing.T) {
assert := assert.New(t)
data := compressTestData
// 不同的压缩解压
tests := []struct {
fn func() ([]byte, error)
encoding string
}{
{
fn: func() ([]byte, error) {
return Get("").Gzip(data)
},
encoding: EncodingGzip,
},
{
fn: func() ([]byte, error) {
return Get("").Brotli(data)
},
encoding: EncodingBrotli,
},
{
fn: func() ([]byte, error) {
return doLZ4Encode(data, 0)
},
encoding: EncodingLZ4,
},
{
fn: func() ([]byte, error) {
dst := doSnappyEncode(data)
return dst, nil
},
encoding: EncodingSnappy,
},
{
fn: func() ([]byte, error) {
return data, nil
},
encoding: "",
},
}
for _, tt := range tests {
result, err := tt.fn()
assert.Nil(err)
assert.NotEmpty(result)
result, err = Get("").Decompress(tt.encoding, result)
assert.Nil(err)
assert.Equal(data, result)
}
_, err := Get("").Decompress("a", nil)
assert.Equal(notSupportedEncoding, err)
}
================================================
FILE: compress/gzip.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package compress
import (
"bytes"
"compress/gzip"
"io/ioutil"
)
// doGunzip gunzip
func doGunzip(buf []byte) ([]byte, error) {
r, err := gzip.NewReader(bytes.NewBuffer(buf))
if err != nil {
return nil, err
}
defer r.Close()
return ioutil.ReadAll(r)
}
func gzipFn(buf []byte, level int) (*bytes.Buffer, error) {
buffer := new(bytes.Buffer)
if level <= 0 || level > gzip.BestCompression {
level = gzip.DefaultCompression
}
w, err := gzip.NewWriterLevel(buffer, level)
if err != nil {
return nil, err
}
defer w.Close()
_, err = w.Write(buf)
if err != nil {
return nil, err
}
return buffer, nil
}
// doGzip gzip function
func doGzip(buf []byte, level int) ([]byte, error) {
buffer, err := gzipFn(buf, level)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
================================================
FILE: compress/gzip_test.go
================================================
package compress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDoGzip(t *testing.T) {
assert := assert.New(t)
// 压缩级别超出则与默认压缩级别一样
tests := []struct {
data []byte
level int
resultSize int
}{
{
data: compressTestData,
level: 0,
resultSize: 660,
},
{
data: compressTestData,
level: 0,
resultSize: 660,
},
{
data: compressTestData,
level: 0,
resultSize: 660,
},
}
for _, tt := range tests {
data, err := doGzip(tt.data, tt.level)
assert.Nil(err)
assert.NotEqual(tt.data, data)
assert.Equal(tt.resultSize, len(data))
}
}
func TestDoGunzip(t *testing.T) {
assert := assert.New(t)
tests := []struct {
data []byte
}{
{
data: compressTestData,
},
}
for _, tt := range tests {
data, err := doGzip(tt.data, 0)
assert.Nil(err)
assert.NotEmpty(tt.data, data)
assert.NotNil(data)
data, err = doGunzip(data)
assert.Nil(err)
assert.Equal(tt.data, data)
}
}
================================================
FILE: compress/lz4.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package compress
import (
"github.com/pierrec/lz4"
)
func doLZ4Encode(data []byte, level int) ([]byte, error) {
buf := make([]byte, len(data))
n, err := lz4.CompressBlock(data, buf, nil)
if err != nil {
return nil, err
}
buf = buf[:n]
return buf, nil
}
func doLZ4Decode(buf []byte) ([]byte, error) {
dst := make([]byte, 10*len(buf))
n, err := lz4.UncompressBlock(buf, dst)
if err != nil {
return nil, err
}
dst = dst[:n]
return dst, nil
}
================================================
FILE: compress/lz4_test.go
================================================
package compress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDoLZ4Decode(t *testing.T) {
assert := assert.New(t)
data := compressTestData
result, err := doLZ4Encode(data, 0)
assert.Nil(err)
assert.NotNil(result)
assert.NotEqual(data, result)
result, err = doLZ4Decode(result)
assert.Nil(err)
assert.Equal(data, result)
}
================================================
FILE: compress/snappy.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package compress
import (
"github.com/golang/snappy"
)
func doSnappyEncode(data []byte) []byte {
dst := []byte{}
dst = snappy.Encode(dst, data)
return dst
}
func doSnappyDecode(buf []byte) ([]byte, error) {
var dst []byte
return snappy.Decode(dst, buf)
}
================================================
FILE: compress/snappy_test.go
================================================
package compress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDoSnappyDecode(t *testing.T) {
assert := assert.New(t)
data := compressTestData
dst := doSnappyEncode(data)
assert.NotNil(dst)
assert.NotEqual(data, dst)
buf, err := doSnappyDecode(dst)
assert.Nil(err)
assert.Equal(data, buf)
}
================================================
FILE: compress/zstd.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package compress
import (
"github.com/klauspost/compress/zstd"
)
func doZSTDEncode(data []byte, level int) ([]byte, error) {
l := zstd.EncoderLevel(level)
if l < zstd.SpeedFastest || l > zstd.SpeedBestCompression {
l = zstd.SpeedDefault
}
encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(l))
if err != nil {
return nil, err
}
return encoder.EncodeAll(data, make([]byte, 0, len(data))), nil
}
func doZSTDDecode(buf []byte) ([]byte, error) {
decoder, err := zstd.NewReader(nil)
if err != nil {
return nil, err
}
return decoder.DecodeAll(buf, nil)
}
================================================
FILE: compress/zstd_test.go
================================================
package compress
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDoZSTDDecode(t *testing.T) {
assert := assert.New(t)
data := compressTestData
dst, err := doZSTDEncode(data, 1)
assert.Nil(err)
assert.NotNil(dst)
assert.NotEqual(data, dst)
buf, err := doZSTDDecode(dst)
assert.Nil(err)
assert.Equal(data, buf)
}
================================================
FILE: config/config.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package config
import (
"errors"
"strings"
"github.com/vicanso/pike/app"
"github.com/vicanso/pike/log"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
type (
// Client client interface
Client interface {
// Get get the data of key
Get() (data []byte, err error)
// Set set the data of key
Set(data []byte) (err error)
// Watch watch change
Watch(OnChange)
// Close close client
Close() error
}
OnChange func()
// PikeConfig pike config
PikeConfig struct {
// YAML 界面展示之用,不需要保存
YAML string `json:"yaml,omitempty" yaml:"-"`
// Version 程序版本
Version string `json:"version,omitempty" yaml:"version,omitempty" `
Admin AdminConfig `json:"admin,omitempty" yaml:"admin,omitempty" validate:"omitempty,dive"`
Compresses []CompressConfig `json:"compresses,omitempty" yaml:"compresses,omitempty" validate:"omitempty,dive"`
Caches []CacheConfig `json:"caches,omitempty" yaml:"caches,omitempty" validate:"omitempty,dive"`
Upstreams []UpstreamConfig `json:"upstreams,omitempty" yaml:"upstreams,omitempty" validate:"omitempty,dive"`
Locations []LocationConfig `json:"locations,omitempty" yaml:"locations,omitempty" validate:"omitempty,dive"`
Servers []ServerConfig `json:"servers,omitempty" yaml:"servers,omitempty" validate:"omitempty,dive"`
}
// AdminConfig admin config
AdminConfig struct {
User string `json:"user,omitempty" yaml:"user,omitempty" validate:"omitempty,min=3"`
Password string `json:"password,omitempty" yaml:"password,omitempty" validate:"omitempty,min=6"`
Remark string `json:"remark,omitempty" yaml:"remark,omitempty"`
}
// CompressConfig compress config
CompressConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty" validate:"required,xName"`
Levels map[string]uint `json:"levels,omitempty" yaml:"levels,omitempty"`
Remark string `json:"remark,omitempty" yaml:"remark,omitempty"`
}
// CacheConfig cache config
CacheConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty" validate:"required,xName"`
Size int `json:"size,omitempty" yaml:"size,omitempty" validate:"required,gt=0" `
HitForPass string `json:"hitForPass,omitempty" yaml:"hitForPass,omitempty" validate:"required,xDuration"`
Store string `json:"store,omitempty" yaml:"store,omitempty" validate:"omitempty,url"`
Remark string `json:"remark,omitempty" yaml:"remark,omitempty"`
}
// UpstreamServerConfig upstream server config
UpstreamServerConfig struct {
Addr string `json:"addr,omitempty" yaml:"addr,omitempty" validate:"required,xAddr"`
Backup bool `json:"backup,omitempty" yaml:"backup,omitempty"`
// Healthy 界面展示使用,不需要保存
Healthy bool `json:"healthy,omitempty" yaml:"-"`
}
// UpstreamConfig upstream config
UpstreamConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty" validate:"required,xName"`
HealthCheck string `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty" validate:"omitempty,xURLPath"`
Policy string `json:"policy,omitempty" yaml:"policy,omitempty" validate:"omitempty,xPolicy"`
EnableH2C bool `json:"enableH2C,omitempty" yaml:"enableH2C,omitempty"`
AcceptEncoding string `json:"acceptEncoding,omitempty" yaml:"acceptEncoding,omitempty" validate:"omitempty,ascii"`
Servers []UpstreamServerConfig `json:"servers,omitempty" yaml:"servers,omitempty" validate:"required,dive"`
Remark string `json:"remark,omitempty" yaml:"remark,omitempty"`
}
// LocationConfig location config
LocationConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty" validate:"required,xName"`
Upstream string `json:"upstream,omitempty" yaml:"upstream,omitempty" validate:"required,xName"`
Prefixes []string `json:"prefixes,omitempty" yaml:"prefixes,omitempty" validate:"omitempty,dive,xURLPath"`
Rewrites []string `json:"rewrites,omitempty" yaml:"rewrites,omitempty" validate:"omitempty,dive,xDivide"`
QueryStrings []string `json:"queryStrings,omitempty" yaml:"queryStrings,omitempty" validate:"omitempty,dive,xDivide"`
RespHeaders []string `json:"respHeaders,omitempty" yaml:"respHeaders,omitempty" validate:"omitempty,dive,xDivide"`
ReqHeaders []string `json:"reqHeaders,omitempty" yaml:"reqHeaders,omitempty" validate:"omitempty,dive,xDivide"`
Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty" validate:"omitempty,dive,hostname"`
ProxyTimeout string `json:"proxyTimeout,omitempty" yaml:"proxyTimeout,omitempty" validate:"omitempty,xDuration"`
Remark string `json:"remark,omitempty" yaml:"remark,omitempty"`
}
// ServerConfig server config
ServerConfig struct {
LogFormat string `json:"logFormat,omitempty" yaml:"logFormat,omitempty"`
Addr string `json:"addr,omitempty" yaml:"addr,omitempty" validate:"required,ascii"`
Locations []string `json:"locations,omitempty" yaml:"locations,omitempty" validate:"required,dive,xName"`
Cache string `json:"cache,omitempty" yaml:"cache,omitempty" validate:"required,xName"`
Compress string `json:"compress,omitempty" yaml:"compress,omitempty" validate:"omitempty"`
// 最小压缩长度
CompressMinLength string `json:"compressMinLength,omitempty" yaml:"compressMinLength,omitempty" validate:"omitempty,xSize"`
// 压缩数据类型
CompressContentTypeFilter string `json:"compressContentTypeFilter,omitempty" yaml:"compressContentTypeFilter,omitempty" validate:"omitempty,xFilter"`
Remark string `json:"remark,omitempty" yaml:"remark,omitempty"`
}
)
var defaultClient Client
var (
ErrUpstreamNotFound = errors.New("upstream of location not found")
ErrLocationNotFound = errors.New("location of server not found")
ErrCacheNotFound = errors.New("cache of server not found")
ErrCompressNotFound = errors.New("compress of server not found")
)
// InitDefaultClient init default client
func InitDefaultClient(url string) (err error) {
if defaultClient != nil {
// 如果关闭出错,仅输出日志
e := defaultClient.Close()
if e != nil {
log.Default().Error("close config client fail",
zap.Error(e),
)
}
}
defaultClient = nil
if strings.HasPrefix(url, "etcd://") {
c, err := NewEtcdClient(url)
if err != nil {
return err
}
defaultClient = c
return nil
}
c, err := NewFileClient(url)
if err != nil {
return
}
defaultClient = c
return
}
func (c *PikeConfig) Validate() error {
err := defaultValidator.Struct(c)
if err != nil {
return err
}
// 判断location中设置的upstream是否存在
for _, l := range c.Locations {
found := false
for _, upstream := range c.Upstreams {
if l.Upstream == upstream.Name {
found = true
}
}
if !found {
return ErrUpstreamNotFound
}
}
// 校验server中的location, cache 以及 compress 是否正确设置
for _, s := range c.Servers {
for _, item := range s.Locations {
notFound := true
for _, l := range c.Locations {
if item == l.Name {
notFound = false
}
}
if notFound {
return ErrLocationNotFound
}
}
foundCache := s.Cache == ""
for _, cacheConfig := range c.Caches {
if cacheConfig.Name == s.Cache {
foundCache = true
}
}
if !foundCache {
return ErrCacheNotFound
}
foundCompress := s.Compress == ""
for _, compressConfig := range c.Compresses {
if compressConfig.Name == s.Compress {
foundCompress = true
}
}
if !foundCompress {
return ErrCompressNotFound
}
}
return nil
}
// GetAdminConfig get admin config
func (p *PikeConfig) GetAdminConfig() AdminConfig {
return p.Admin
}
// Read read pike config
func Read() (config *PikeConfig, err error) {
data, err := defaultClient.Get()
if err != nil {
return
}
config = &PikeConfig{}
err = yaml.Unmarshal(data, config)
if err != nil {
return
}
config.YAML = string(data)
return
}
// Write write pike config
func Write(config *PikeConfig) (err error) {
err = config.Validate()
if err != nil {
return
}
config.Version = app.GetVersion()
data, err := yaml.Marshal(config)
if err != nil {
return
}
return defaultClient.Set(data)
}
// Close close the client
func Close() (err error) {
if defaultClient != nil {
err = defaultClient.Close()
}
defaultClient = nil
return
}
// Watch watch the change
func Watch(onChange OnChange) {
defaultClient.Watch(onChange)
}
================================================
FILE: config/config_test.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package config
import (
"math/rand"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidate(t *testing.T) {
assert := assert.New(t)
// location配置的upstream不存在
c := &PikeConfig{
Locations: []LocationConfig{
{
Name: "location-test",
Upstream: "upstream-test",
},
},
}
err := c.Validate()
assert.Equal(ErrUpstreamNotFound, err)
c = &PikeConfig{
Servers: []ServerConfig{
{
Addr: ":3015",
Locations: []string{
"location-test",
},
Cache: "cache-test",
},
},
}
err = c.Validate()
assert.Equal(ErrLocationNotFound, err)
c = &PikeConfig{
Upstreams: []UpstreamConfig{
{
Name: "upstream-test",
Servers: []UpstreamServerConfig{
{
Addr: "http://127.0.0.1:3015",
},
},
},
},
Locations: []LocationConfig{
{
Name: "location-test",
Upstream: "upstream-test",
},
},
Servers: []ServerConfig{
{
Addr: ":3015",
Locations: []string{
"location-test",
},
Cache: "cache-test",
},
},
}
err = c.Validate()
assert.Equal(ErrCacheNotFound, err)
c = &PikeConfig{
Caches: []CacheConfig{
{
Name: "cache-test",
Size: 100,
HitForPass: "1m",
},
},
Upstreams: []UpstreamConfig{
{
Name: "upstream-test",
Servers: []UpstreamServerConfig{
{
Addr: "http://127.0.0.1:3015",
},
},
},
},
Locations: []LocationConfig{
{
Name: "location-test",
Upstream: "upstream-test",
},
},
Servers: []ServerConfig{
{
Addr: ":3015",
Locations: []string{
"location-test",
},
Cache: "cache-test",
Compress: "compress-test",
},
},
}
err = c.Validate()
assert.Equal(ErrCompressNotFound, err)
c = &PikeConfig{
Caches: []CacheConfig{
{
Name: "cache-test",
Size: 100,
HitForPass: "1m",
},
},
Compresses: []CompressConfig{
{
Name: "compress-test",
},
},
Upstreams: []UpstreamConfig{
{
Name: "upstream-test",
HealthCheck: "/ping",
Policy: "first",
Servers: []UpstreamServerConfig{
{
Addr: "http://127.0.0.1:3015",
},
},
},
},
Locations: []LocationConfig{
{
Name: "location-test",
Upstream: "upstream-test",
Prefixes: []string{
"/api",
},
Rewrites: []string{
"/api/:/$1",
},
QueryStrings: []string{
"id:1",
},
Hosts: []string{
"test.com",
},
RespHeaders: []string{
"X-Resp-Id:1",
},
ReqHeaders: []string{
"X-Req-Id:2",
},
},
},
Servers: []ServerConfig{
{
Addr: ":3015",
Locations: []string{
"location-test",
},
Cache: "cache-test",
Compress: "compress-test",
CompressMinLength: "1kb",
CompressContentTypeFilter: "text|json",
},
},
}
err = c.Validate()
assert.Nil(err)
}
func TestInitDefaultClient(t *testing.T) {
assert := assert.New(t)
file := strconv.Itoa(int(rand.Int31()))
err := InitDefaultClient("etcd://127.0.0.1:2379/" + file)
assert.Nil(err)
err = InitDefaultClient(os.TempDir() + "/" + file)
assert.Nil(err)
err = Close()
assert.Nil(err)
}
func TestReadWriteConfig(t *testing.T) {
assert := assert.New(t)
file := strconv.Itoa(int(rand.Int31()))
err := InitDefaultClient(os.TempDir() + "/" + file)
assert.Nil(err)
c := &PikeConfig{
Compresses: []CompressConfig{
{
Name: "compress-test",
},
},
}
err = Write(c)
assert.Nil(err)
currentConfig, err := Read()
assert.Nil(err)
assert.Equal(c.Compresses[0].Name, currentConfig.Compresses[0].Name)
}
================================================
FILE: config/etcd_client.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// etcd client for config
package config
import (
"context"
"crypto/tls"
"net/url"
"strings"
"time"
"github.com/coreos/etcd/clientv3"
)
// etcdClient etcd client
type etcdClient struct {
c *clientv3.Client
key string
Timeout time.Duration
}
const (
defaultEtcdTimeout = 5 * time.Second
)
// NewEtcdClient create a new etcd client
func NewEtcdClient(uri string) (client *etcdClient, err error) {
u, err := url.Parse(uri)
if err != nil {
return
}
conf := clientv3.Config{
Endpoints: strings.Split(u.Host, ","),
DialTimeout: defaultEtcdTimeout,
}
if u.User != nil {
conf.Username = u.User.Username()
conf.Password, _ = u.User.Password()
}
cert := u.Query().Get("cert")
key := u.Query().Get("key")
if cert != "" && key != "" {
tlsConfig := tls.Config{}
tlsConfig.Certificates = make([]tls.Certificate, 1)
// TODO 支持更多种形式的拉取证书,如HTTP的方式
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, err
}
conf.TLS = &tlsConfig
}
// TODO 后续支持从querystring中配置更多的参数
c, err := clientv3.New(conf)
if err != nil {
return
}
client = &etcdClient{
c: c,
key: u.Path,
}
return
}
func (ec *etcdClient) context() (context.Context, context.CancelFunc) {
d := ec.Timeout
if d == 0 {
d = defaultEtcdTimeout
}
return context.WithTimeout(context.Background(), d)
}
// Get get data from etcd
func (ec *etcdClient) Get() (data []byte, err error) {
ctx, cancel := ec.context()
defer cancel()
resp, err := ec.c.Get(ctx, ec.key)
if err != nil {
return
}
kvs := resp.Kvs
if len(kvs) == 0 {
return
}
data = kvs[0].Value
return
}
// Set set data to etcd
func (ec *etcdClient) Set(data []byte) (err error) {
ctx, cancel := ec.context()
defer cancel()
_, err = ec.c.Put(ctx, ec.key, string(data))
return
}
// Watch watch config change
func (ec *etcdClient) Watch(onChange OnChange) {
ch := ec.c.Watch(context.Background(), ec.key)
// 只监听有变化则可
for range ch {
onChange()
}
}
// Close close etcd client
func (ec *etcdClient) Close() error {
return ec.c.Close()
}
================================================
FILE: config/etcd_client_test.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEtcdClient(t *testing.T) {
assert := assert.New(t)
etcdClient, err := NewEtcdClient("etcd://root:123456@127.0.0.1:2379/test-etcd")
assert.Nil(err)
defer etcdClient.Close()
go etcdClient.Watch(func() {
})
data := []byte("abc")
err = etcdClient.Set(nil)
assert.Nil(err)
result, err := etcdClient.Get()
assert.Nil(err)
assert.Empty(result)
err = etcdClient.Set(data)
assert.Nil(err)
result, err = etcdClient.Get()
assert.Nil(err)
assert.Equal(data, result)
}
================================================
FILE: config/file_client.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// file client for config
package config
import (
"io/ioutil"
"os"
"github.com/fsnotify/fsnotify"
"github.com/vicanso/pike/log"
"go.uber.org/zap"
)
// fileClient file client
type fileClient struct {
file string
watcher *fsnotify.Watcher
}
const defaultPerm os.FileMode = 0600
// NewFileClient create a new file client
func NewFileClient(file string) (client *fileClient, err error) {
f, err := os.OpenFile(file, os.O_RDONLY|os.O_CREATE, defaultPerm)
if err != nil {
return
}
defer f.Close()
watcher, err := fsnotify.NewWatcher()
if err != nil {
return
}
client = &fileClient{
file: file,
watcher: watcher,
}
return
}
// Get get data from file
func (fc *fileClient) Get() (data []byte, err error) {
return ioutil.ReadFile(fc.file)
}
// Set set data to file
func (fc *fileClient) Set(data []byte) (err error) {
return ioutil.WriteFile(fc.file, data, defaultPerm)
}
// Watch watch config change
func (fc *fileClient) Watch(onChange OnChange) {
err := fc.watcher.Add(fc.file)
if err != nil {
log.Default().Error("add watch fail",
zap.String("file", fc.file),
zap.Error(err),
)
return
}
for {
select {
case event, ok := <-fc.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
onChange()
}
case err, ok := <-fc.watcher.Errors:
if !ok {
return
}
if err != nil {
log.Default().Error("watch error",
zap.String("file", fc.file),
zap.Error(err),
)
}
}
}
}
// Close close file client
func (fc *fileClient) Close() error {
return fc.watcher.Close()
}
================================================
FILE: config/file_client_test.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFileClient(t *testing.T) {
assert := assert.New(t)
fileClient, err := NewFileClient(os.TempDir() + "/test-file")
assert.Nil(err)
defer fileClient.Close()
go fileClient.Watch(func() {
})
data := []byte("abc")
err = fileClient.Set(nil)
assert.Nil(err)
result, err := fileClient.Get()
assert.Nil(err)
assert.Empty(result)
err = fileClient.Set(data)
assert.Nil(err)
result, err = fileClient.Get()
assert.Nil(err)
assert.Equal(data, result)
}
================================================
FILE: config/validate.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package config
import (
"net/url"
"reflect"
"regexp"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/go-playground/validator/v10"
us "github.com/vicanso/upstream"
)
var defaultValidator = validator.New()
func init() {
addAlias("xName", "max=20")
addValidate("xDuration", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
_, err := time.ParseDuration(value)
return err == nil
})
addValidate("xAddr", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
urlInfo, err := url.Parse(value)
if err != nil {
return false
}
return contains([]string{"http", "https"}, urlInfo.Scheme)
})
addValidate("xURLPath", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
return value != "" && value[0] == '/'
})
addValidate("xDivide", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
arr := strings.Split(value, ":")
return len(arr) == 2
})
addValidate("xSize", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
_, err := humanize.ParseBytes(value)
return err == nil
})
addValidate("xFilter", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
if len(value) > 1000 {
return false
}
_, err := regexp.Compile(value)
return err == nil
})
addValidate("xPolicy", func(fl validator.FieldLevel) bool {
value, ok := toString(fl)
if !ok {
return false
}
return contains([]string{
us.PolicyFirst,
us.PolicyRandom,
us.PolicyRoundRobin,
us.PolicyLeastconn,
}, value)
})
}
// toString 转换为string
func toString(fl validator.FieldLevel) (string, bool) {
value := fl.Field()
if value.Kind() != reflect.String {
return "", false
}
return value.String(), true
}
func contains(arr []string, str string) bool {
found := false
for _, item := range arr {
if item == str {
found = true
break
}
}
return found
}
func addValidate(tag string, fn validator.Func, args ...bool) {
err := defaultValidator.RegisterValidation(tag, fn, args...)
if err != nil {
panic(err)
}
}
func addAlias(alias, tags string) {
defaultValidator.RegisterAlias(alias, tags)
}
================================================
FILE: docs/alarm.md
================================================
---
description: 告警回调,通过配置相应的回调地址可及时获知程序异常
---
当程序出现异常时,如更新配置失败、upstream节点异常等,若启动指定了告警的回调地址,程序则会以POST的形式调用告警地址,内容如下:
```json
{
"application": "pike",
"hostname": "tiger",
"category": "类别",
"message": "告警消息"
}
```
category有如下的类型:
- `upstream` 当upstream下的某个server检测失败时,则触发告警
- `config` 当更新config失败时,则触发告警
- `admin` 当admin管理后台启动失败时,则触发告警
建议生产使用时,通过告警回调发送短信、邮件等方式及时获取告警内容
================================================
FILE: docs/cache-handler.md
================================================
---
description: 缓存处理
---
HTTP缓存使用了LRU缓存,能提供高效的缓存读取能力及有效控制缓存过大。为了减少缓存中锁的影响,使用了128个LRU组成缓存桶,每次根据hash选择对应的LRU,提升性能。需要注意,缓存只针对`GET`与`HEAD`请求,其它的请求都是不可缓存请求。
<p align="center">
<img src="./images/flow.png"/>
</p>
## 缓存的获取
- 根据请求的URL生成key(Method + Host + RequestURI)
- 使用该key通过MemHash生成hash值取余获取对应的缓存桶
- 从缓存桶中获取缓存数据
## 缓存有效期
HTTP缓存的有效期仅支持从`Cache-Control`响应头中获取,获取有效期的流程如下:
- 如果响应头有`Set-Cookie`,则返回缓存有效期为0
- 如果响应头无`Cache-Control`,则返回缓存有效期为0
- 如果响应头中`Cache-Control`包含`no-cache`,`no-store`或者`private`,则返回有效期为0
- 如果响应头中`Cache-Control`包含`s-maxage`,则优先根据`s-maxage`获取缓存有效期
- 如果响应头中`Cache-Control`包含`max-age`,则根据`max-age`获取缓存有效期
- 如果响应头中有`Age`字段,则最终的缓存有效期需减去`Age`
## 缓存状态
- `passed` 如果请求非HEAD与GET请求,其缓存状态则为passed(并不缓存数据),直接跳过缓存转发至后端服务
- `fetching` 当请求对应的key无法查找到缓存时,其缓存状态则为fetching,表示无缓存转发至后端服务。当获取该请求响应时,如果可缓存,则将相关数据缓存。如果不可缓存时,则缓存hit for pass(只缓存状态不需要缓存数据)
- `hit` 当请求对应的key可以获取到缓存数据,且该数据是可缓存,则直接返回
- `hitForPass` 当请求对应的key获取到缓存数据,且该数据是hit for pass时,则直接转发至后端服务
## 缓存建议
Pike的设计保证了当缓存不存在时,相同的请求只会有一个请求至upstream,整体设计主要是为了应对高并发时系统性能下降,并不建议使用它来提升一个本来响应慢的请求。在使用时,也建议使用短缓存(Cache-Control中设置max-age或s-maxage),避免需要手工删除数据,非静态文件建议缓存时长不超过5分钟,静态文件(url中有相应的版本号)不超过1小时。
================================================
FILE: docs/error.md
================================================
---
description: 程序处理异常时的各出错信息
---
pike程序处理出错时,均返回category: "pike"的出错信息,可根据此分类判断是否系统错误,主要的错误如下:
- `ErrInvalidResponse` 响应数据异常时使用,主要是程序无法获取正常的响应,http状态码为`503`,出错信息为`Invalid response`
- `ErrCacheDispatcherNotFound` 无法获取配置的缓存时使用,由于配置了不存在的缓存导致,使用管理后台配置时会有相应的校验,因此一般不会触发。http状态码为`503`,出错信息为`Available cache dispatcher not found`
- `ErrLocationNotFound` 无法获取可用的Location,由于配置的Location无符合该请求时触发。http状态码为`503`,出错信息为`Available location not found`
- `ErrUpstreamNotFound` 无法获取可用的Upstream,如果配置的所有Upstream的状态检测均不通过,转发至相应的Upstream时触发。http状态码为`502`,出错信息为`Available upstream not found`
================================================
FILE: docs/flow.drawio
================================================
<mxfile host="Electron" modified="2020-11-25T06:10:42.289Z" agent="5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="E9hEKhKTk29Dx5lVZ34i" version="13.7.9" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7Vzbdps4FP0arzXzkCzu4MckTdJ22k7bZJrkUTGyTYMtF+TE7tePhCUbIRmIzc2N89AiIYPQ3vucoyNBz7yYLK4jMBt/Rj4Me4bmL3rmu55h6FrfIP/RmuWqxrOcVcUoCnzWaFNxE/yG/Jesdh74MBYaYoRCHMzEygGaTuEAC3UgitCL2GyIQvGuMzCCUsXNAIRy7V3g4zF7CsPd1L+HwWjM76w7/dWZCeCN2ZPEY+Cjl1SVedkzLyKE8OposriAIR08Pi53H5Z34acn5/rjt/gX+O/8n9svP05WF7t6zU/WjxDBKa720gzcZxDO2XixZ8VLPoARmk99SC+i9czzMZ6E5FAnhz8hxksGOJhjRKpQhMdohKYg/ITQjLUboilmzXRahlP/jAJLyo8hGjytqq6CMGT3ICXW3iOlGEfoaY0dvcAaCNo4BI8wPAeDp1HS0QsUooicmqIppJfyCRnYs2w6d7mpPS85tgyDGM2jAcxpZzKKg2gE867HRET7l+IpQ+4aognE0ZI0iGAIcPAskhkwTYzW7Ta4kwMG/StoYEo0eH97+5XUfIe/5jDGOaSggLyMAwxvZiAZmRdiR0SipAlAnvF8FII4ZvAVoPs6dJ5hhOEidzz5WYeJmls1VnzZmAid636cMg+WVhMCloTAF/TWpEgwjpb39GKnNi8+rDtCCu8WQmm5E0kKJezIEv54//zZv//87cOPIf61BA8j798hM7fFEmYU43QqrWh2pa8oII+1aYKGwxhi0dTzNozd63uxW5tahrarZ2O/yjB33fNSZFaOjGlLbH4gMUCWzsWMEAmvpLSC+ilhwEWA7zlpyPHDhl2ktOETLSzT5LpPFx5ETm6hYaGYWuKpV5KnjDwn2mlfswT+nOg7cZeMBlimGswo32IFtblRztCWx5DbaJ5t71p2hs2rHpQW0g78z0MmRf8PcYJ8HP81RbQPFDFDI+aRuhfg/90zr2RrP0aTx3lc7F8FI0xN/BWYBCEdlPcwfIY4GACFFwZhMJqSwoCwEUZq605uGUxHpORsSreJ9IjDqtE7uyKw63LaPWsK9+zV5Z77tRi0Q/PgOxrGiu0eN0aFhk+3Xuehd7NyO3lo0xIZbvWb9dC6TOhW3HPDLrM0dcoyp/LpmbI33M/lzw6IccXi8Eomnppg4g3CM3ZiEvj+CkkYB7/BY3IpOu7MVZPr2uc9+91209AzzGHypwQrn3tZf7BOArGO9NJ5FpWf0E5NIxOo7Kdg3pvML+qLFHQZ2SRUGIDBGFICLoKY4nCMDrZHBxafbjQRHagZ7f3p1jRvAlzsh1tLduV1OwXWdRKNBz4ZlgDTjqBh0pui3JdWrL7aRJBJYBmqEFmVwbLrEkE7lOfz+/Wc/iF1Rj2/byCmyG3YERm0ZKN2AGzXvGDT9tAqaQ+7ZQ7lzPNNYg55CBJjgGmccUaOhxAPxtTnH4RNtO3WbaK8sPIWAoOqM5PNCEHO2l2MEYqpBsC0lywIE0X4STH59xkEYTJVMbT5jMTHEEwOQxiO2aAwzsbgh4Yc/efz9fz7v+DSfDp5KLPu3B1dZPhZoVA8WSjK4doyDW5GF/Lk5oq6AVLlAwyoV4jQ5OA04BqtOwfZ4HRYBJVxXi8bJhl2p9wD3zFUsAZwKFm4Nfv2zsKdaKemIK09k3DZRcdKs3DKsXCto/DS1OhWXObKuwkOOPvNuVaJ7iyD+wwuGms/7fEOWU1JT5enn0kCfD3TPKa+U5BLy4atp745t95aEKOY4h5C0k+XY85V8nuM8WyT9DmEAN5sPbtju+1QnWVD14Uu5kJ58N5CsLHT3jM7Y1lt18vwI7WXrCrnZ8iRzR0IaK+GydYwn+67IYNN/BEIu6lKy8t4pNZVacoRxSFuPhXV3ebm01zZ1rD71NP6thjU6vZ+UW39YazpvIWopzTodr8Woy5vo+tnEtvZOLfuje5yTuiAp6ZmlSkh23P7+6l2/QqiaAwsT7xCfaK25cz3Ed0VupkXBMxKkHYzQDeFsyHjnOQgxqlQbJa8snbMRWzPRdiOIvJT5SLc2hYVdQmeJudj2qlb7faUFdYpyWvJXwPrktx/Fy5M6mYVfv21kzVLEwNEz8l/UaigfT2TOz6G0sYaYvRnaJrsK2Arpx3OupimOHSuplC5XtP8Tkk5eXrXjuq7IN7tmwVKaHdf6e6ForwJd5s4ZhFaLIUz++tEEQOVG/6c7GTGxOht6+RA3vBpTyeKTzUo23lt6oSnCNvJXPHjcpmrnZFOhVCOIyS+6FTDKM59kdJXGAVkyGmQXPXWrb3YU02EJGc+Mi/B9LN2pLrMR97jZ6dMbLPwn7F+W4OXyKDm9UvOmapYv1Xi2IVNTdT+xAzAOgc/s1TRV3z0xVKMvVHX2PPXDI8uOtf1ljGyTW2QVftosx3Yanorpvtwr3TSFtxbsoX8fRn6PTpqMhwwoe5shJOBo+N39ITbPaGuqRILjbpCvd3tHJog4v0XfNuTseqNyHpi4/3wLpGDIGhVmJ5rQEa6Ku+gklFteQfdeLshpa61HlPmfMqxMH/WxLccqwfBkDSgmlV5ChSq+JqjGoVDWjkuvSC8N1JWZi3fyVsKVOc9iHkRl54NyxWvUck6sRpVOaV6RJWiKiLi5kVo3UOV37m12ZupOa+bvymynR2OBnlSo+PRIO+mFA1Ku0A6GQgarrj+pGtOyVdDdggESXHz2fOVDjcfjzcv/wc=</diagram></mxfile>
================================================
FILE: docs/modules.drawio
================================================
<mxfile host="Electron" modified="2020-11-30T01:27:25.250Z" agent="5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="HscShrD6E4d00Fu-lVfB" version="13.7.9" type="device"><diagram id="3YllJVyIKX4QYfRWK-ka" name="第 1 页">7Ztbd6I6FMc/jY+dJYSLPra2p5112jUztbPO6XlLJQJTJDRErf30J9RwTRQvROh0noRNbvz3Lzs7UXtgNHu9JjDy7rCDgp7ed1574LKn61p/qLOPxLJaWwaGtTa4xHd4odww9t9QWpNb576D4lJBinFA/ahsnOAwRBNaskFC8LJcbIqDcq8RdJFgGE9gIFr/8R3q8bcw+7n9Bvmul/as9fmTGUwLc0PsQQcvCyZw1QMjgjFdX81eRyhIxEt1Wdf7a8PTbGAEhXSXCjc/nPOXKdbf0Fd6Nv27/2LfP54ZfHALGMz5G/PR0lUqgUvwPBJ74wNYIELRq8wX8CltIX9dxgnCM0TJipXjtUzAB8ERMW1+v8wF11NZvaLYqRcgd7KbtZ3rwC64FHvIAuyDZdku8+FicXHArtroyrTR6rVh0oQOSlrReuBi6fkUjSM4SZ4uWZxgNo/OAv7YgbGXld1R0MxBVUE3UnamlSnL7otKajIlgTIldUHJ6zc/2qJmv15NZQLqZf00U5DPlqgHTFXiGYJ4F6Sj0mWLQle0MwXtxiGMolVH9augZ4kz97TyWYJ8t/8ZHdWuyl7r4g0ky4cVsG4vppi9e7IiBpi8P7Fe5kl2xMQBlgVA8ia5yXL553vdp9QwwrOIoDhOH7BBPlULM9u6r9Rc8RyTlpbdE1OCn9GIjyzEIUrG6wdBxQQD3w3Z7YS5ETH7ReIon6WT5/zBzHecpBspD2Vijs0ttiAhIGBIEFCWQZigPoNQnXQOKjmnJK8CsrxKV5ZzGl3IqzLX1MO0KlNSK5yyNMoQ06hmIwqceOjThJM9CEhXZ6vdcGLsEE5UrMX7K1Xd6Ep2IDLlNFuZdGISfenHEaSMeDGZ/s05rvfO4KRYi0l6R7E2y8LZQMRafkRhq5JOTNBvHh6+M8vt/c/PhnW9d06L9Q5Ha93A2rJKwg3FZU6zDFG5gaFKOXHnxKnmGcrn4rrePaflerjJO/cojnAYiw7qBuYDs6Sjpkk4101RSUMV56a48P2MGLoIzrZIuMOGSJRw03Zyc45QiaaGZC+dHb0X1bIa4O7bt4d4uny+Jnc3k1/6NVpYd8aZtkM8RY6LxvwWE+phF4cwuMqtlTmbl7nFOOLq/UKUrvgXdHBOsUzbpKOSshKxYzwnE7TN/3wmUUhctJV1U+4rggJI/UV5JM1jKk74WzxhHeOwdUx1rXOYiinZx8HU77sXNvoRX9Hz4fnVDdBeFuAs/Ta6QKm0nH4iSLcNsqC6PFc4NaDGsEVAN7uprFR2ktyyWBbo2my2P/jcBUfOSV71O/bfzxfTnZZR9hOoHnmuYwWvVXFBNozDERbPwcaILCQHOSef7ZXTdm3QNsCSuIjDqe82IZXkywlBvY1S2V2b6qCNqc7EIqt/ef33m8fk5ouZ3l6+Fh9ernrFzea+IUL63vqOy/uxoeQo51gfJw5vFrnxOKyfLg5vQ6cQXO6g38im4KjQonUuCu9wwh17MEouAz98PgxDUY/i+6YH00cip+k1yK2ngoCc0JCttcvusN1wn0f4x1KAPzbcF8P45oSg8UhU/fFNtpzXeFNsCBzG1zkhcFUoFiUF4i0DrvZj9/cbV7k8u1iPoFFGtVaODlRD2vwyaA2/lM92D8ZPbEoVgJKeahCsraEIwpYT4w8CodFvDEKxKVUQSnqqgbC2hiIIxV+d/oFQdjDTGIRiU6oglPRUA2FtDUUQmn8g3AFCa9AYhGJTqiCU9FQDYW2NfSFkt/mf5NbF878agqv/AQ==</diagram></mxfile>
================================================
FILE: docs/modules.md
================================================
---
description: 程序中的主要模块
---
Pike有6个主要模块,下面对这些模块一一介绍。
<p align="center">
<img src="./images/modules.png"/>
</p>
## Compress
压缩模块主要提供数据解压与压缩服务,数据解压可针这几类压缩算法:gzip, br, lz4, zstd, snappy,用于在接收到upstream返回的数据时,根据其数据压缩类型,解压出原始数据。压缩则只提供gzip与br压缩,因为压缩的数据是响应返回至客户端(如浏览器),而现在的客户端支持的压缩算法主要为以上两种。
什么场景下upstream需要返回压缩的数据呢,,主要考虑的是以下场景:
- pike与upstream是在同一内网,网络传输不存在瓶颈问题,则upstream返回数据时不需要压缩
- pike与upstream部署在不同的IDC,网络通过专线传输,此时则可以考虑使用zstd或者snappy压缩响应数据,减少专线带宽的使用
- pike与upstream的访问通过公网访问,由于公网的网络性能较差,此时尽可量考虑使用br或者zstd压缩响应数据,提升性能
下面表示展示了压缩算法执行100次压缩解压的测试结果(原始数据160KB的json数据):
<p align="center">
<img src="./images/compression-performance.jpg"/>
</p>
由上图可以看出,br压缩使用的时间较长,一般建议使用6则可,而内网或专线等网络较好而又希望减少带宽占用的,可以在pike与upstream使用snappy,减少带宽占用,节约成本。
具体测试代码[compression-performance](https://github.com/vicanso/compression-performance)。
## Cache
缓存模块主要针对GET、HEAD的相关请求,保存缓存的数据或hit for pass状态,主要由以下小模块组成:
- `Dispatcher` 该模块使用memhash对请求生成hash,从初始化的LRU列表中获取对应的LRU
- `LRU` LRU缓存,缓存http cache对象
- `HTTPCache` http cache对象,控制当相同的请求访问时,如果状态未知,只允许一个请求转发至upstream,其它的访问等待。而在获取到状态后,如果是可缓存则直接返回,若是hit for pass则转发至upstream.
- `HTTPResponse` http response对象,根据请求的客户端接受的编码以及响应数据长度,选择最合适的压缩方式并返回
缓存的详细流程请阅读[缓存模块](./cache-handler.md),缓存模块使用压缩模式实现数据压缩。
## Upstream
Upstream模块,该模块实现对配置的upstream服务检测是否可用,按指定的策略返回对应的server地址,实现的功能如下:
- 定时检测配置的各服务地址是否正常(配置health check的则使用http访问,如果未配置,则以tcp的方式检测端口)
- 根据配置的策略选择可用的服务地址
## Location
Location模块,该模块根据配置的host与prefix规则,判断请求是否属于该location,如果属于则将请求转发至其下的upstream,实现的功能如下:
- 根据host与prefix判断请求是否属于该location
- 根据配置的rewrite,在转发前修改url,在完成后恢复
- 根据配置的query string以及request header,将当前配置添加至请求中
- 获取响应后将配置的response header添加至响应头中
## Config
Config模块,该模块主要实现配置的读写,支持使用文件与etcd的形式保存配置,并可检测配置变化实时更新配置。
## Server
Server模块,该模块监控端口,在接收新的请求时,通过各中间件完成缓存的读取或转发,实现的功能如下:
- `Error` 出错中间件将出错转换为对应的json响应或text响应
- `Fresh` 304中间件处理,根据请求头与响应头判断数据是否无修改
- `Responder` 响应中间件,使用`HTTPResponse`根据客户端响应适当的数据
- `Cache` 缓存中间件,获取当前请求对应的缓存,如果有响应则设置缓存,否则则转至下一中间件
- `Proxy` proxy中间件,根据当前请求获取对应的location,转发至相应的upstream服务器地址,获取成功后则设置响应数据
================================================
FILE: docs/performance.md
================================================
---
description: 性能测试
---
测试机器:8U 8GB内存
测试数据:数据原始长度约为140KB
测试场景:客户端支持gzip、 br以及不支持压缩三种场景,并发请求数设置为1000,测试时长为1分钟
测试结论:当客户端可接受gzip或br压缩时,测试的结果均非常接近,而客户端不接受压缩时,需要先解压数据,性能则大幅度下降
## GZIP
```bash
wrk -c1000 -t10 -d1m -H 'Accept-Encoding: gzip' --latency http://127.0.0.1:3015/repos
Running 1m test @ http://127.0.0.1:3015/repos
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 17.12ms 17.05ms 241.93ms 83.49%
Req/Sec 6.96k 1.17k 12.88k 73.49%
Latency Distribution
50% 16.78ms
75% 29.72ms
90% 38.34ms
99% 66.55ms
4153511 requests in 1.00m, 40.89GB read
Requests/sec: 69123.45
Transfer/sec: 696.85MB
```
## BR
```bash
wrk -c1000 -t10 -d1m -H 'Accept-Encoding: br' --latency http://127.0.0.1:3015/repos
Running 1m test @ http://127.0.0.1:3015/repos
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 16.65ms 16.27ms 181.87ms 64.06%
Req/Sec 7.08k 1.06k 17.58k 70.18%
Latency Distribution
50% 16.56ms
75% 29.11ms
90% 37.47ms
99% 62.46ms
4223664 requests in 1.00m, 36.57GB read
Requests/sec: 70302.18
Transfer/sec: 623.26MB
```
## 不支持压缩
```bash
wrk -c1000 -t10 -d1m --latency http://127.0.0.1:3015/repos
Running 1m test @ http://127.0.0.1:3015/repos
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 319.12ms 421.34ms 2.00s 81.56%
Req/Sec 555.57 193.17 2.54k 69.97%
Latency Distribution
50% 32.36ms
75% 589.84ms
90% 965.91ms
99% 1.61s
331687 requests in 1.00m, 44.85GB read
Socket errors: connect 0, read 0, write 0, timeout 4348
Requests/sec: 5518.90
Transfer/sec: 764.11MB
```
================================================
FILE: docs/questions.md
================================================
---
description: 对于pike的各类常见疑问
---
## 何时需要配置缓存持久化
pike主要是用于缓存热点接口(可缓存),设计上保证了当缓存不存在或过期时,相同的请求只能有一个转发至后端。缓存也建议使用短缓存(不超过5分钟,甚至是1分钟以下),短缓存场景下持久化的效果不大,因此期望后端应用是可以支撑正常的请求量或者有熔断机制。
对于新的实例无缓存时导致后端服务无法支持过大的请求量,可使用缓存持久化。现支持使用badger(文件)或redis的方式实现缓存持久化。若是机器内存不足,配置较小的LRU配合badger的方式以支持更大的接口缓存量,选择redis则可以多实例共享缓存但性能比badger差。而机器内存足够则建议不使用持久化,如果担心pike崩溃重新恢复时,后端服务无法支撑过大的请求量,可以启动多个pike实例,前置通过nginx转发至pike,而nginx的转发策略则使用url_hash,这样就算其中一个pike崩溃也只是部分缓存失效。
## 为什么缓存的数据使用单独的压缩配置
如果数据是可缓存的,会单独使用配置为`bestCompression`的压缩配置(默认的配置为gzip:9, br:6),虽然可以配置相同的配置覆盖,但不建议调整。由于缓存的数据都会被使用多次(如果缓存基本不会被重复使用,那么pike也无意义了),因此数据仅压缩一次则被使用多次,而选择更高的压缩级别可减少与客户端的网络(公网)传输耗时,提升用户体验。br的压缩对CPU的占用特别大,而最高的压缩级别11占用了过多的CPU,而减少的数据并不非常明显。
在实际环境中,由于机器资源可能为实体机等资源,一般CPU、内存等都是冗余的,而带宽则较为方便的动态调节,因此建议根据CPU的使用资源调整不同的压缩级别,以达到资源与性能的平衡(节约带宽也能节约成本)。
## 为什么缓存的有效期仅基于`Cache-Control`
varnish对于接口缓存有效期判断主要基于`Cache-Control`,但它还支持`Expires`以及一些基于状态码(如307, 302响应码,则缓存时间为-1)来生成缓存有效期,此种处理符合RFC的各规范要求。由于规则较多,有不少的开发者并不清楚varnish的缓存有效期是怎么生成的,而且varnish还支持default ttl,更增加了使用风险(缓存了不应该缓存的数据有可能导致泄露客户数据)。
pike的缓存只基于响应的`Cache-Control`,完全由接口开发人员控制缓存的处理,更准确高效(开发人员才清楚接口是否可缓存以及缓存时长),避免了在varnish使用中由开发者要求运维人员针对特别接口配置缓存(varnish也支持通过Cache-Control配置,但实际有不少使用者直接通过不同的url指定不同缓存的方式)。
## 管理后台为什么是基于flutter web
pike的管理后台并没有太复杂的功能,都仅是一些表单的填写,而刚好以前学习了flutter,flutter web则是一种学习尝试,仅此而已。
================================================
FILE: docs/response.drawio
================================================
<mxfile host="Electron" modified="2020-12-06T00:45:03.026Z" agent="5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="AIcvpBXua0KuKNYm5qFg" version="13.7.9" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7V3Zdto4GH4aLpNjS964bEiTdtrp6UzSaXupYAU8MRZjiwB5+pFjGbAksMMiBUwuciwhZPlfvn+TTAf2RrPbFI2Hf5IQxx1ghbMOvO4AEAQ++593zIsOz/OKjkEahUWXvey4i14w77R47yQKcVYZSAmJaTSudvZJkuA+rfShNCXT6rBHElfvOkYDLHXc9VEs9/6MQjrkjwX8Zf8nHA2G5Z1tr1t8MkLlYP4k2RCFZLrSBT92YC8lhBZXo1kPxzntSrr8/Dz/GX998m7/+Cv7D/24+nL/7Z+LYrKbt3xl8QgpTuh+pwbF1M8onnB68Wel85KAKZkkIc4nsTrwakhHMbu02eW/mNI5ZziaUMK6SEqHZEASFH8lZMzHPZKE8mF23sZJ+CFnLGs/xKT/VHTdRHHM78FafHzAWhlNydOCd/kEC0bkg2P0gOMr1H8avC60R2KSso8SkuB8qpAJA3+W5eI+LnuvGtKW8yAjk7SPN4yDXMRROsCb5uNKlK9vRU45524xGWGaztmAFMeIRs9VYUZcJwaLcUu+swvO+jeIAZTE4JatHlh/42xMkgxvEIqcIdNhRPHdGL1SZspgpCooqwLAnvFqEKMs4+yr4e7buPOMU4pnG+lZfmpxpeaoFvDmdAkRdqn3wxV4cKwDccCROPCboeZZF3fRRa+hLpamqlYZufCUCyhlh9+nsaryib+TKKGrs3qVaS9cQdbI42OWa6UgaYv1bS98riR834gke/XsO27pTBgNf/HvvTZ+59+79N2yfT1b3DVvzbeCqP0Jrb2T0F74wX6k1qsC6QUI6qRWmgMKYOyKkl8Qg39rORGTDTRfGTbOB2Tr1wqFtZY+6tp1CeNdUBnPLooV7FUXPUkXP2e5G/0SjWVzMCSjh0lWb4ErepBr2Q0aRXH+WJ9w/Ixp1EcKO43iaJCwRp9JNk7VCsZuGSUD1vKWrftXhWYm7YD2266yxvYVBtxSGPDgUAY8aIKhx42QtQZggaHWpVtBUb0QWsrGKobGNz++9J5unJfb2ZeL624ynU2tizfafesSsr8qhFi7oegbUNIW0MixmqHkvoCp28hDbamXADR7CSoR34+bYF0GnuVWwVWfkANByCE8kCsA1PdZ6woI411LgytQ0l30BR7Ssyew1hMAXY2egNKqyMmUnVFSiYMKvFxhdC0qKnFhnxBVZnvrrDBQ81dP5qtc5Qq3eilGFOfSQun4dSFZTQ7MOokcmKPSnEMlwZSSIPPi1DVnk1va0LZr0JxNq1xh1jXuk9GYqUtd4vLQuiLpQFPmNNcV27SuyDm7VuoKaGhkHJOqUhoZ3bxRxyp1ocrR8NTfkafbRQ4CEnhWTeQgVpr8/UYOmygoBg591B9i9BArfJkjiR8OgOxC/OB6puMHOdHSSmR3GqJAYBTZDfFmyyzU0TDVCLSLySe/pj4kjg88DdAubxS4G5JJHObwvtb7PqN7aa111omU/JPre6o0OiMDrbJGInJOLMaP+AP/YBSFYQEvOIteXq18wUcu9mxe96rjXudzMUTJOAMPSXxBPzxHJr6joD04FO19ifbXeVp/fbCqY3PT4YPVwDUdrCoy2a30aQJN5m8nZsmV7F5b0jq+zn2Aak2RqwetsQ++Aqe02gdbdq4UmzhOgvYs0K3Q3vVdifZQK+0bbUI8DdrDKu09ReFFL+27rbTGtmKvpXqgUXtsyyHDp/v77513sz1//xri+IIHa9osL84AtUxD/KYaYjQL55jhziyiv1auV6orrLXMwOWNZQJuy13mxyMKEOwoClvl7nxxb3dNWUYcH+x5b/dGGq4m7ybjMZO9TrGry0JJWF6GiKKco7MoozLAtzaj5wRVxtnG6zWmchnb7bU2DyRNkyDajkSqbYqZmv0ebcr7rwY1FgVDNkWAGlhTDxLHB3veJLyRhkqbUpwa4laFN852pZFdAcYrRcBpZcyxeEtErX2AJu1DObF0Zj5F09yFI+Fc1q4jT5eLYTk0vmMYyLmRdiqN4r0T6oGuUafKDHf25lQtGu/Vo2osB4Y8qm4VQNw6j0oYX1ay1433xKje1eCBAblkd96S09zRcsw7WrIL3U4j4jY1IkbrIdA/OiNidSqR+bsPzBtLAjSaowFyzbiaTz2D7hrQdVUVNb2nZKEZjF1kTcFxpU2BtvrLbioplziu0o2O0InFxKrNHHpjYmgob3Sk9QjQPQ7Fko8F3RYJ3ZNVLQ+8O9Vyj871PLbDn7Bp+tesOkLvDLKH4GoBc8a4Kif1xWLaOaQo0Rm+u5ACSNw7yfM9rlOlvG/eLjZ6++FJbN4WiQ8Uh9q0bt6GsmN4srQXIAcqDoxopb0jp/5PlvZB9dCC6m1RemnfnsM6Iu3Ld46Yo317Duv4opvTNXxQymnRCfIFscvNvgofU+sJQUdOb54w8QVPR4H4eokve5knS3xH3Clh+mys0+z9zydJfE9xxkAr8Uvmt4H4nng69nDEZ83lz3cV24GWv4EGP/4P</diagram></mxfile>
================================================
FILE: docs/response.md
================================================
---
description: 请求响应处理
---
请求响应的处理主要分两步,一是从upstream中获取响应(或从缓存中),二是根据响应与客户端选择符合的响应数据。HTTP响应数据主要有以下字段:
- `GzipBody` gzip压缩的body
- `BrBody` br压缩的body
- `RawBody` 原始未压缩的body
- `Header` HTTP响应头
<p align="center">
<img src="./images/response.png"/>
</p>
## 从Upstream中获取响应
参考上面的流程图,从upstream中获取响应之后,主要根据响应头的Encoding以及是否可缓存生成不同的响应数据。
- 如果upstream的响应数据是gzip或br压缩,直接生成对应的GzipBody或BrBody
- 如果upstream的响应数据是未压缩的,直接生成对应的RawBody
- 生成HTTP Response之后,判断该请求是否缓存并且可压缩(根据Content-Type判断),如果可缓存压缩则生成GzipBody以及BrBody,并清除RawBody
需要注意,对于不可缓存的数据,生成HTTP Response时,upstream返回的数据并不处理,直接保存。而对于可缓存的数据,则根据其是否可压缩生成gzip与br的数据,并清除RawBody,因此RawBody并不会与压缩的数据同时存在。
## 从HTTP Response中响应数据
参考上面的流程图,从HTTP Response中响应客户端的主要流程如下:
- 客户端支持br压缩,而且已有br压缩数据,则直接返回响应
- 客户端支持gzip压缩,而且已有gzip压缩数据,则直接返回响应
- 响应数据不应该被压缩,则直接返回原始数据
- 客户支持br压缩,则对数据压缩后返回
- 客户端支持gzip压缩,则对数据压缩后返回
- 客户端不支持压缩,则返回原始数据
需要注意,对于可缓存压缩数据,在生成缓存时会预压缩(同时生成br与gzip压缩),因此可以减少压缩数据对性能的损耗(一次压缩多次使用)
================================================
FILE: docs/start.md
================================================
---
description: 如何开始使用pike
---
Pike是纯go的项目,可以使用各平台的执行文件启动或者已打包好的docker镜像(vicanso/pike),配置信息支持保存在文件或etcd中,生产环境中建议使用etcd便于多实例部署。
## 启动参数
```bash
Pike is a http cache server
Usage:
pike [flags]
Flags:
--admin string The address of admin web page, e.g.: :9013
--alarm string The alarm request url, alarm will post to the url, e.g.: http://192.168.1.2:3000/alarms
--config string The config of pike, support etcd or file, etcd://user:pass@192.168.1.2:2379,192.168.1.3:2379/pike or /opt/pike.yml (default "pike.yml")
-h, --help help for pike
--log string The log path, e.g.: /var/pike.log or lumberjack:///tmp/pike.log?maxSize=100&maxAge=1&compress=true
```
如上所示pike的启动参数如下:
- admin 管理后台监听地址,用于启动管理后台,建议最少其中一个实例启用管理后台,方便使用WEB管理后台编辑配置
- alarm 告警回调服务地址,当upstream的服务器检测失败或配置更新失败等时回调,用于告警通知
- config 配置地址,可以用于etcd或者file的形式,建议在生产环境中使用etcd,如果不配置则直接使用文件形式,文件为pike.yml
- log 日志目录配置,可以指定单一文件或使用lumberjack按时按文件大小分割日志并压缩
首次启动指定管理后台监听地址为:9013,未指定配置地址(使用pike.yml),启动成功后可以看到在执行目录下生成了一个空白的新文件pike.yml(如果该文件已存在则不新建),之后可以打开`http://127.0.0.1:9013/#/`进行配置。
```bash
./pike --admin=:9013
```
## 压缩参数配置
- `Name` 压缩配置名称,用于区分每个压缩配置,可根据不同的应用场景配置不同的压缩参数,一般只使用一个通用配置则可
- `Gzip Level` gzip的压缩级别,如果CPU较为紧张,则可以配置为默认的压缩级别6,如果CPU较为空闲,建议直接配置为最高压缩级别9,减少网络带宽的占用
- `Br Level` brotli的压缩级别,由于br的压缩率较高,占用CPU较大,因此一般配置为6则可,具体根据CPU的使用状况可以选择更优的配置方式
- `Remark` 备注
<p align="center">
<img src="./images/add-compress.png"/>
</p>
需要注意,对于缓存数据的压缩会直接使用默认的`bestCompression`的压缩配置,该配置的压缩级别为gzip:9, br:6,如果需要覆盖默认的配置,则直接新配置名为`bestCompression`的配置则可覆盖。缓存的数据只压缩一次而可使用多次,可以选择较高的压缩级别,对于常规的压缩配置,br的压缩级别配置为6则可,如果CPU占用较多,可以选择更小的值,具体各压缩级别耗时可查看模块-压缩模块的说明。
## 缓存参数配置
- `Name` 缓存配置名称,用于区分每个缓存配置,对于请求量特别大的服务,可单独使用一个缓存,其它的服务则共用一个缓存则可
- `Size` 缓存数量大小,指定LRU缓存的最大数量,可根据服务的缓存情况以及机器内存选择较为合适的值,一般设置为51200已能满足大部分应用的需求,如果内存较少则设置为更小的值
- `HitForPass` 设置hit for pass的缓存时长,对于不可缓存的GET、HEAD请求,为了后续快速判断请求是否hit for pass,缓存中也有保存该请求的缓存状态(hitForPass)。
- `Store` 设置缓存持久化存储的方式,暂只支持badger,如`badger:///tmp/badger`表示将缓存保存至`/tmp/badger`目录。如果内存较为空余,可设置LRU的Size为较大的值而不设置Store。
- `Remark` 备注
为什么会有需要hit for pass的场景?考虑一下以下场景,由于产品刚好被下架处理,因此请求产品详情信息时,该接口返回了出错(http status: 400,cache control: no-cache),因此访问该产品的接口缓存为hit for pass,而后续产品上架了,接口正常响应,缓存时长为cache-control: max-age=60,此时接口应该可缓存的。而由于hit for pass未过期,因此只能等hit for pass过期后接口才变为可缓存。
因此在设置hit for pass的时候需要考虑应用的具体出错处理逻辑,Cache-Control是否无论怎样都不会变化(有一种处理是同样的参数,无论成功失败均使用同样的Cache-Control,这样保证无论成功还是失败,接口均是缓存,避免过多请求),如果是不变的,可以将hit for pass设置为较长的有效期,否则应该选择更短的有效期。
<p align="center">
<img src="./images/add-cache.png"/>
</p>
### 缓存的Store配置
- `badger`:使用badger缓存数据,配置格式为:`badger:///tmp/badger`,表示将数据缓存在`/tmp/badger`目录下。此模式下缓存会以文件的形式持久化,可减少LRU缓存的数据避免占用过多的内存。需要注意如果是启动多个实例,那么多实例间的缓存无法共享
- `redis`:使用redis缓存数据,配置格式为:`redis://[:pwd@]host1:port1[,...hostN:portN]/[?timeout=3s&master=master]`。密码`pwd`为只选参数。对于`sentinel`还需要指定master参数。
- `mongodb`:使用mongodb缓存数据,配置格式为mongodb的connection string形式,如:`mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]`,增加支持timeout参数指定请求超时。如`mongodb://localhost:27017/pike?timeout=5s`,连接localhost:27017并指定使用db:pike保存缓存数据
### redis配置
- `普通模式`:`redis://:pwd@127.0.0.1:6379/?db=1&timeout=5s&prefix=test`,连接本地127.0.0.1:6379的redis,密码为pwd,使用db:1,并设置超时时间为5秒,key的前缀为test
- `sentinel`:`redis://:pwd@127.0.0.1:6379,127.0.0.1:6380/?master=master&mode=sentinel&timeout=5s&prefix=test`,以sentinel的模式连接redis,密码为pwd,master name为master,并设置超时时间为5秒,key的前缀为test
- `cluster`:`redis://:pwd@127.0.0.1:6379,127.0.0.1:6380/?mode=cluster&timeout=5s&prefix=test`,以cluster的模式连接redis,密码为pwd,并设置超时时间为5秒,key的前缀为test
## Upstream配置
- `Name` upstream的配置名称,用于区分每个upstream配置
- `Health Check` 健康检测的url路径,对于HTTP服务尽量使用特定的url的响应来检测upstream是否可用,如果未配置,则检测地址的端口是否有监听
- `Policy` 服务器列表的选择策略,支持四种方式`roundRobin`,`random`, `first`与`leastConn`,一般选择`roundRobin`则可
- `Enable H2C` 是否启用HTTP/2 over TCP,upstream的服务支持h2c模式,则可以启用此模式,pike与upstream的服务则使用h2c方式访问
- `Accept Encoding` 设置可接受的编码,如果需要节约pike与upstream服务之间访问的网络带宽,可以添加此配置,pike支持编码:`gzip`,`br`,`lz4`,`zst`, 以及`snz`
- `Servers.Addr` 服务地址,以http(s)://ip:port的形式配置
- `Servers.Backup` 是否备用服务地址,如果设置为备用,则只要在主服务有一个可用时,均不会使用备用服务
- `Remark` 备注
<p align="center">
<img src="./images/add-upstream.png"/>
</p>
## Location配置
- `Name` location的配置名称,用于区分每个location配置
- `Upstream` 选择对应的upstream
- `Prefixes` 配置对应的前缀,可配置多个,前缀的匹配优先级高于host
- `Hosts` 配置对应的host,可配置多个
- `Rewrites` 转发请求时,需要重写的URL的规则,配置格式为`key:value`的形式,以`:`分割
- `QueryStrings` 转发请求时添加至url中querystring,配置格式为`key:value`的形式,以`:`分割
- `RespHeaders` 响应头配置,将在所有的响应中添加响应头,配置格式为`key:value`的形式,以`:`分割
- `ReqHeaders` 请求头配置,将在所有的请求中添加请求头,配置格式为`key:value`的形式,以`:`分割
- `ProxyTimeout` 请求超时配置,用于控制请求转发至upstream的服务中的超时,根据实际场景配置,如:30s,1m等等
- `Remark` 备注
<p align="center">
<img src="./images/add-location.png"/>
</p>
### Rewrite规则
重写的规则与nginx类似,支持使用正则匹配,如下面的例子:
- `/api/*:/$1` $1表示*部分,最终的处理就是转发时将/api前缀删除
- `/rest/*/user/*:/$1/$2` $1表示第一个*,$2表示第二个*,最终的处理就是转发的时候将/rest与/user替换
虽然通过正则可以实现各类的重写,但是不建议使用过于复杂的正则,尽可能少用或只用重写来处理前缀,规范url减少重写。
### ENV获取配置
`QueryStrings`,`RespHeaders`以及`ReqHeaders`均支持从ENV中获取值的处理方式,如:`DC:$DC`,$DC表示从ENV中获取DC对应的值。
## Server配置
- `Addr` 监听地址
- `Locations` 对应的location列表
- `Cache` 缓存,根据应用访问量选择合适的缓存
- `Compress` 压缩,根据带宽与CPU的考虑,选择合适的压缩
- `Compress Min Length` 最小压缩长度,此值不要设置太少,因为压缩小数据效果并不明显,而且浪费CPU。一般建议设置为1kb,如果是内网间调用,建议此值可以调更大的值
- `Compress Content Filter` 压缩数据类型筛选,指定针对哪些数据类型压缩,默认值为:`text|javascript|json|wasm|xml`,可按应用的需求自定义配置或不匹配。
- `Log Format` 请求日志格式化配置,如`{remote} {when-iso} {:proxyTarget} {method} {uri} {proto} {status} {<x-status} {size-human} {referer} {userAgent}`,配置规则参考[elton logger](https://github.com/vicanso/elton/blob/master/docs/middlewares.md#logger),日志的输出对于性能会有所影响
- `Remark` 备注
日志格式化配置中大部分都是通用的属性,需要注意以下的配置字段:
- `:proxyTarget` 转发的目标地址
- `<x-status` 接口响应的缓存状态
<p align="center">
<img src="./images/add-server.png"/>
</p>
当Server配置完成后,可以使用`curl http://addr/ping`来检测该Server是否启动成功,`/ping`默认由pike处理而不会转发至upstream
### Location匹配
location列表中,按是否匹配prefix与host排序,其优先级是 prefix+host > prefix > host > 无配置,从列表中按顺序一个个location匹配,匹配则该请求由此location处理,如果所有均不匹配则出错。
## Admin配置
- `Account` 登录账号
- `Password` 登录密码
设置配置成功后重启pike,之后每次使用都需要登录校验,建议在首次配置则设置。
## 缓存列表
暂未支持查询当前缓存列表功能,仅可用于删除缓存
<p align="center">
<img src="./images/caches.png"/>
</p>
## 非实时生效配置
- `缓存配置` 由于缓存是多个LRU组成,因此如果调整缓存大小会导致缓存失败,而且锁的处理也比较麻烦,因此缓存更新非实时生效,只能重启应用
- `Server配置的Log` 日志的输出是在Server创建时生成,如果后续有调整,只能重启应用
- `Admin配置` admin配置非实时生效,因此在初始创建时建议配置
================================================
FILE: entrypoint.sh
================================================
#!/bin/sh
set -e
if [ "${1:0:1}" = '-' ]; then
set -- pike "$@"
fi
exec "$@"
================================================
FILE: go.mod
================================================
module github.com/vicanso/pike
go 1.16
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
replace github.com/coreos/bbolt => go.etcd.io/bbolt v1.3.5
require (
github.com/andybalholm/brotli v1.0.3
github.com/coreos/etcd v3.3.25+incompatible
github.com/dgraph-io/badger/v3 v3.2103.0
github.com/dustin/go-humanize v1.0.0
github.com/frankban/quicktest v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/go-playground/validator/v10 v10.6.1
github.com/go-redis/redis/v8 v8.11.0
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golang/snappy v0.0.3
github.com/google/uuid v1.2.0 // indirect
github.com/klauspost/compress v1.13.1
github.com/pierrec/lz4 v2.6.1+incompatible
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.21.5
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
github.com/vicanso/elton v1.4.2
github.com/vicanso/elton-jwt v1.2.1
github.com/vicanso/hes v0.3.9
github.com/vicanso/upstream v0.2.0
go.mongodb.org/mongo-driver v1.5.3
go.uber.org/atomic v1.8.0
go.uber.org/automaxprocs v1.4.0
go.uber.org/zap v1.18.1
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/yaml v1.2.0 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.25+incompatible h1:0GQEw6h3YnuOVdtwygkIfJ+Omx0tZ8/QkVyXI4LkbeY=
github.com/coreos/etcd v3.3.25+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.0 h1:abkD2EnP3+6Tj8h5LI1y00dJ9ICKTIAzvG9WmZ8S2c4=
github.com/dgraph-io/badger/v3 v3.2103.0/go.mod h1:GHMCYxuDWyzbHkh4k3yyg4PM61tJPFfEGSMbE3Vd5QE=
github.com/dgraph-io/ristretto v0.0.4-0.20210309073149-3836124cdc5a h1:1cMMkx3iegOzbAxVl1ZZQRHk+gaCf33Y5/4I3l0NNSg=
github.com/dgraph-io/ristretto v0.0.4-0.20210309073149-3836124cdc5a/go.mod h1:MIonLggsKgZLUSt414ExgwNtlOL5MuEoAJP514mwGe8=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I=
github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-redis/redis/v8 v8.11.0 h1:O1Td0mQ8UFChQ3N9zFQqo6kTU2cJ+/it88gDB+zg0wo=
github.com/go-redis/redis/v8 v8.11.0/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/flatbuffers v1.12.0 h1:/PtAHvnBY4Kqnx/xCQ3OIV9uYcSFGScBsWI3Oogeh6w=
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil/v3 v3.21.5 h1:YUBf0w/KPLk7w1803AYBnH7BmA+1Z/Q5MEZxpREUaB4=
github.com/shirou/gopsutil/v3 v3.21.5/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU=
github.com/tidwall/gjson v1.8.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M=
github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek=
github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZc=
github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/vicanso/elton v1.3.0/go.mod h1:psz+FXn7NJtq6lm36Q3KoeU8XGf4Rxj6iO2/O/LIToM=
github.com/vicanso/elton v1.4.2 h1:G8Yhq1Lht1frZFDMXvUR2RX+U6dXGlhklAhjvRgVJek=
github.com/vicanso/elton v1.4.2/go.mod h1:BFhCB2ke3uPLo0Ids8wgYmNeq5nbivqvHtfwIX8PY/c=
github.com/vicanso/elton-jwt v1.2.1 h1:Fau6AqglfJTXxk81guTOe/Fsf2j13xX4mugzP/KpK4U=
github.com/vicanso/elton-jwt v1.2.1/go.mod h1:Ge/OvZmUMz8896P5MivqHJBegsmxBt5fibQo3rP17JU=
github.com/vicanso/fresh v1.0.0/go.mod h1:gr1RKSFxQ1OnQHzUMBHCigifni7KrXveJjWCTlPjICA=
github.com/vicanso/hes v0.3.5/go.mod h1:B0l1NIQM/nYw7owAd+hyHuNnAD8Nsx0T6duhVxmXUBY=
github.com/vicanso/hes v0.3.6/go.mod h1:B0l1NIQM/nYw7owAd+hyHuNnAD8Nsx0T6duhVxmXUBY=
github.com/vicanso/hes v0.3.9 h1:IO21yElX6Xp3w+Lc1O2QIySrJj2jEhnl5dWbqbDYunc=
github.com/vicanso/hes v0.3.9/go.mod h1:B0l1NIQM/nYw7owAd+hyHuNnAD8Nsx0T6duhVxmXUBY=
github.com/vicanso/intranet-ip v0.0.1 h1:cYS+mExFsKqewWSuHtFwAqw/CO66GsheB/P1BPmSTx0=
github.com/vicanso/intranet-ip v0.0.1/go.mod h1:bqQ6VUhxdz0ipSb1kzd6aoZStlp+pB7CTlVmVhgLAxA=
github.com/vicanso/keygrip v1.1.0/go.mod h1:tfB5az1yqold78zotkzNugk3sV+QW5m71CFz3zg9eeo=
github.com/vicanso/keygrip v1.2.1 h1:876fXDwGJqxdi4JxZ1lNGBxYswyLZotrs7AA2QWcLeY=
github.com/vicanso/keygrip v1.2.1/go.mod h1:tfB5az1yqold78zotkzNugk3sV+QW5m71CFz3zg9eeo=
github.com/vicanso/upstream v0.2.0 h1:qwMyFoa9ROmb3Dnz3iIsgbsB4btE4idGOp8zsAsu5xs=
github.com/vicanso/upstream v0.2.0/go.mod h1:HIPeCB653T+1hwW9siMek8S7paa8xq6q6y+/J//5ffE=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.mongodb.org/mongo-driver v1.5.3 h1:wWbFB6zaGHpzguF3f7tW94sVE8sFl3lHx8OZx/4OuFI=
go.mongodb.org/mongo-driver v1.5.3/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.8.0 h1:CUhrE4N1rqSE6FM9ecihEjRkLQu8cDfgDyoOs83mEY4=
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0=
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
================================================
FILE: hooks/pre-commit
================================================
#!/bin/sh
#
make lint && make test
================================================
FILE: location/location.go
================================================
// MIT License
// Copyright (c) 2020 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of
gitextract_52j68_1c/
├── .air.toml
├── .github/
│ └── workflows/
│ ├── build.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── SUMMARY.md
├── app/
│ ├── app.go
│ └── app_test.go
├── asset/
│ └── asset.go
├── cache/
│ ├── cache.go
│ ├── cache_test.go
│ ├── dispatcher.go
│ ├── dispatcher_test.go
│ ├── http_cache.go
│ ├── http_cache_test.go
│ ├── http_response.go
│ ├── http_response_test.go
│ ├── memhash.go
│ └── memhash_test.go
├── compress/
│ ├── brotli.go
│ ├── brotli_test.go
│ ├── compress.go
│ ├── compress_test.go
│ ├── gzip.go
│ ├── gzip_test.go
│ ├── lz4.go
│ ├── lz4_test.go
│ ├── snappy.go
│ ├── snappy_test.go
│ ├── zstd.go
│ └── zstd_test.go
├── config/
│ ├── config.go
│ ├── config_test.go
│ ├── etcd_client.go
│ ├── etcd_client_test.go
│ ├── file_client.go
│ ├── file_client_test.go
│ └── validate.go
├── docs/
│ ├── alarm.md
│ ├── cache-handler.md
│ ├── error.md
│ ├── flow.drawio
│ ├── modules.drawio
│ ├── modules.md
│ ├── performance.md
│ ├── questions.md
│ ├── response.drawio
│ ├── response.md
│ └── start.md
├── entrypoint.sh
├── go.mod
├── go.sum
├── hooks/
│ └── pre-commit
├── location/
│ ├── location.go
│ └── location_test.go
├── log/
│ └── log.go
├── main.go
├── pike.yml
├── schedule/
│ └── schedule.go
├── server/
│ ├── admin.go
│ ├── cache.go
│ ├── cache_test.go
│ ├── proxy.go
│ ├── proxy_test.go
│ ├── responder.go
│ ├── responder_test.go
│ ├── server.go
│ └── server_test.go
├── store/
│ ├── badger.go
│ ├── badger_test.go
│ ├── mongo.go
│ ├── mongo_test.go
│ ├── redis.go
│ ├── redis_test.go
│ ├── store.go
│ └── store_test.go
├── test/
│ └── main.go
├── upstream/
│ ├── upstream.go
│ └── upstream_test.go
├── util/
│ ├── util.go
│ └── util_test.go
└── web/
├── assets/
│ ├── AssetManifest.json
│ ├── FontManifest.json
│ ├── NOTICES
│ ├── fonts/
│ │ └── MaterialIcons-Regular.otf
│ └── packages/
│ └── fluttertoast/
│ └── assets/
│ ├── toastify.css
│ └── toastify.js
├── flutter_service_worker.js
├── index.html
├── main.dart.js
├── manifest.json
└── version.json
SYMBOL INDEX (434 symbols across 62 files)
FILE: app/app.go
type Info (line 38) | type Info struct
function init (line 63) | func init() {
function SetBuildInfo (line 75) | func SetBuildInfo(build, id, ver, buildBy string) {
constant MB (line 88) | MB = 1024 * 1024
function bytesToMB (line 90) | func bytesToMB(value uint64) string {
function GetVersion (line 95) | func GetVersion() string {
function GetInfo (line 100) | func GetInfo() *Info {
function UpdateCPUUsage (line 137) | func UpdateCPUUsage() error {
FILE: app/app_test.go
function TestSetSetBuildInfo (line 10) | func TestSetSetBuildInfo(t *testing.T) {
function TestUpdateCPUUsage (line 19) | func TestUpdateCPUUsage(t *testing.T) {
function TestGetInfo (line 26) | func TestGetInfo(t *testing.T) {
FILE: asset/asset.go
function GetFS (line 33) | func GetFS() embed.FS {
function ReadFile (line 38) | func ReadFile(file string) ([]byte, error) {
FILE: cache/cache.go
function byteSliceToString (line 35) | func byteSliceToString(b []byte) string {
function uint32ToBytes (line 40) | func uint32ToBytes(value int) []byte {
function readUint32ToInt (line 47) | func readUint32ToInt(buffer *bytes.Buffer) (int, error) {
function uint64ToBytes (line 57) | func uint64ToBytes(value int64) []byte {
function readUint64ToInt64 (line 64) | func readUint64ToInt64(buffer *bytes.Buffer) (int64, error) {
function GetDispatcher (line 76) | func GetDispatcher(name string) *dispatcher {
function RemoveHTTPCache (line 81) | func RemoveHTTPCache(name string, key []byte) {
function convertConfigs (line 85) | func convertConfigs(configs []config.CacheConfig) []DispatcherOption {
function ResetDispatchers (line 100) | func ResetDispatchers(configs []config.CacheConfig) {
FILE: cache/cache_test.go
function TestConvertConfig (line 33) | func TestConvertConfig(t *testing.T) {
function TestDefaultDispatcher (line 53) | func TestDefaultDispatcher(t *testing.T) {
function TestUint32ToBytes (line 67) | func TestUint32ToBytes(t *testing.T) {
function TestUint64ToBytes (line 75) | func TestUint64ToBytes(t *testing.T) {
FILE: cache/dispatcher.go
constant defaultZoneSize (line 39) | defaultZoneSize = 128
type httpLRUCache (line 43) | type httpLRUCache struct
method getCache (line 76) | func (lru *httpLRUCache) getCache(key []byte) (*httpCache, bool) {
method addCache (line 88) | func (lru *httpLRUCache) addCache(key []byte, hc *httpCache) {
method removeCache (line 93) | func (lru *httpLRUCache) removeCache(key []byte) {
type dispatcher (line 48) | type dispatcher struct
method getLRU (line 137) | func (d *dispatcher) getLRU(key []byte) *httpLRUCache {
method GetHTTPCache (line 145) | func (d *dispatcher) GetHTTPCache(key []byte) *httpCache {
method RemoveHTTPCache (line 164) | func (d *dispatcher) RemoveHTTPCache(key []byte) {
method GetHitForPass (line 181) | func (d *dispatcher) GetHitForPass() int {
type dispatchers (line 55) | type dispatchers struct
method Get (line 197) | func (ds *dispatchers) Get(name string) *dispatcher {
method RemoveHTTPCache (line 210) | func (ds *dispatchers) RemoveHTTPCache(name string, key []byte) {
method Reset (line 230) | func (ds *dispatchers) Reset(opts []DispatcherOption) {
type DispatcherOption (line 59) | type DispatcherOption struct
function newHTTPLRUCache (line 67) | func newHTTPLRUCache(size int) *httpLRUCache {
function NewDispatcher (line 98) | func NewDispatcher(option DispatcherOption) *dispatcher {
function NewDispatchers (line 186) | func NewDispatchers(opts []DispatcherOption) *dispatchers {
FILE: cache/dispatcher_test.go
function TestLRUGetCache (line 31) | func TestLRUGetCache(t *testing.T) {
function TestDispatcher (line 51) | func TestDispatcher(t *testing.T) {
function TestDispatchers (line 70) | func TestDispatchers(t *testing.T) {
FILE: cache/http_cache.go
type Status (line 40) | type Status
method String (line 79) | func (i Status) String() string {
constant StatusUnknown (line 44) | StatusUnknown Status = iota
constant StatusFetching (line 46) | StatusFetching
constant StatusHitForPass (line 48) | StatusHitForPass
constant StatusHit (line 50) | StatusHit
constant StatusPassed (line 52) | StatusPassed
constant defaultHitForPassSeconds (line 56) | defaultHitForPassSeconds = 300
type httpCache (line 60) | type httpCache struct
method Get (line 110) | func (hc *httpCache) Get() (status Status, response *HTTPResponse) {
method Bytes (line 128) | func (hc *httpCache) Bytes() (data []byte, err error) {
method FromBytes (line 154) | func (hc *httpCache) FromBytes(data []byte) (err error) {
method initFromStore (line 188) | func (hc *httpCache) initFromStore() (err error) {
method saveToStore (line 200) | func (hc *httpCache) saveToStore() (err error) {
method get (line 212) | func (hc *httpCache) get() (status Status, done chan struct{}, data *H...
method HitForPass (line 257) | func (hc *httpCache) HitForPass(ttl int) {
method Cacheable (line 281) | func (hc *httpCache) Cacheable(resp *HTTPResponse, ttl int) {
method Age (line 307) | func (hc *httpCache) Age() int {
method GetStatus (line 314) | func (hc *httpCache) GetStatus() Status {
method IsExpired (line 321) | func (hc *httpCache) IsExpired() bool {
function nowUnix (line 75) | func nowUnix() int64 {
function NewHTTPCache (line 95) | func NewHTTPCache() *httpCache {
function NewHTTPStoreCache (line 102) | func NewHTTPStoreCache(key []byte, store store.Store) *httpCache {
FILE: cache/http_cache_test.go
function TestCacheStatusString (line 11) | func TestCacheStatusString(t *testing.T) {
function TestHTTPCacheBytes (line 21) | func TestHTTPCacheBytes(t *testing.T) {
function TestHTTPCacheGet (line 43) | func TestHTTPCacheGet(t *testing.T) {
function TestHTTPCacheAge (line 122) | func TestHTTPCacheAge(t *testing.T) {
function TestHTTPCacheGetStatus (line 131) | func TestHTTPCacheGetStatus(t *testing.T) {
function TestHTTPCacheIsExpired (line 140) | func TestHTTPCacheIsExpired(t *testing.T) {
FILE: cache/http_response.go
type HTTPResponse (line 56) | type HTTPResponse struct
method Bytes (line 108) | func (resp *HTTPResponse) Bytes() (data []byte, err error) {
method FromBytes (line 164) | func (resp *HTTPResponse) FromBytes(data []byte) (err error) {
method shouldCompressed (line 229) | func (resp *HTTPResponse) shouldCompressed() bool {
method GetRawBody (line 245) | func (resp *HTTPResponse) GetRawBody() (rawBody []byte, err error) {
method Compress (line 262) | func (resp *HTTPResponse) Compress() (err error) {
method getBodyByAcceptEncoding (line 299) | func (resp *HTTPResponse) getBodyByAcceptEncoding(acceptEncoding strin...
method Fill (line 348) | func (resp *HTTPResponse) Fill(c *elton.Context) (err error) {
function cloneHeaderAndIgnore (line 73) | func cloneHeaderAndIgnore(header http.Header) http.Header {
function NewHTTPResponse (line 82) | func NewHTTPResponse(statusCode int, header http.Header, encoding string...
FILE: cache/http_response_test.go
function TestCloneHeaderAndIgnore (line 17) | func TestCloneHeaderAndIgnore(t *testing.T) {
function TestHTTPResponseMarshal (line 30) | func TestHTTPResponseMarshal(t *testing.T) {
function TestNewHTTPResponse (line 63) | func TestNewHTTPResponse(t *testing.T) {
function TestShouldCompressed (line 124) | func TestShouldCompressed(t *testing.T) {
function TestGetRawBody (line 179) | func TestGetRawBody(t *testing.T) {
function TestCompress (line 215) | func TestCompress(t *testing.T) {
function TestGetBodyByAcceptEncoding (line 255) | func TestGetBodyByAcceptEncoding(t *testing.T) {
function TestFill (line 349) | func TestFill(t *testing.T) {
function BenchmarkMarshalHTTPResponse (line 402) | func BenchmarkMarshalHTTPResponse(b *testing.B) {
function BenchmarkHTTPResponseToBytes (line 425) | func BenchmarkHTTPResponseToBytes(b *testing.B) {
FILE: cache/memhash.go
function NanoTime (line 11) | func NanoTime() int64
function CPUTicks (line 15) | func CPUTicks() int64
type stringStruct (line 17) | type stringStruct struct
function memhash (line 24) | func memhash(p unsafe.Pointer, h, s uintptr) uintptr
function MemHash (line 29) | func MemHash(data []byte) uint64 {
function MemHashString (line 37) | func MemHashString(str string) uint64 {
function FastRand (line 44) | func FastRand() uint32
FILE: cache/memhash_test.go
function TestMemHash (line 9) | func TestMemHash(t *testing.T) {
FILE: compress/brotli.go
constant defaultBrQuality (line 33) | defaultBrQuality = 6
function brotliEncode (line 36) | func brotliEncode(buf []byte, level int) (*bytes.Buffer, error) {
function doBrotli (line 51) | func doBrotli(buf []byte, level int) ([]byte, error) {
function doBrotliDecode (line 60) | func doBrotliDecode(buf []byte) ([]byte, error) {
FILE: compress/brotli_test.go
function TestDoBrotli (line 9) | func TestDoBrotli(t *testing.T) {
function TestDoBrotliDecode (line 41) | func TestDoBrotliDecode(t *testing.T) {
FILE: compress/compress.go
constant EncodingGzip (line 36) | EncodingGzip = "gzip"
constant EncodingBrotli (line 37) | EncodingBrotli = "br"
constant EncodingLZ4 (line 38) | EncodingLZ4 = "lz4"
constant EncodingSnappy (line 39) | EncodingSnappy = "snz"
constant EncodingZSTD (line 40) | EncodingZSTD = "zst"
type compressSrv (line 44) | type compressSrv struct
method GetLevel (line 146) | func (srv *compressSrv) GetLevel(encoding string) int {
method SetLevels (line 155) | func (srv *compressSrv) SetLevels(levels map[string]int) {
method Decompress (line 165) | func (srv *compressSrv) Decompress(encoding string, data []byte) ([]by...
method Gzip (line 184) | func (srv *compressSrv) Gzip(data []byte) ([]byte, error) {
method Gunzip (line 190) | func (srv *compressSrv) Gunzip(data []byte) ([]byte, error) {
method Brotli (line 195) | func (srv *compressSrv) Brotli(data []byte) ([]byte, error) {
method BrotliDecode (line 201) | func (srv *compressSrv) BrotliDecode(data []byte) ([]byte, error) {
method LZ4Decode (line 206) | func (srv *compressSrv) LZ4Decode(data []byte) ([]byte, error) {
method SnappyDecode (line 211) | func (srv *compressSrv) SnappyDecode(data []byte) ([]byte, error) {
method ZSTDDecode (line 216) | func (srv *compressSrv) ZSTDDecode(data []byte) ([]byte, error) {
type CompressOption (line 47) | type CompressOption struct
type compressSrvs (line 51) | type compressSrvs struct
method Get (line 97) | func (cs *compressSrvs) Get(name string) *compressSrv {
method Reset (line 110) | func (cs *compressSrvs) Reset(opts []CompressOption) {
constant BestCompression (line 56) | BestCompression = "bestCompression"
function NewServices (line 72) | func NewServices(opts []CompressOption) *compressSrvs {
function NewService (line 85) | func NewService() *compressSrv {
function convertConfigs (line 120) | func convertConfigs(configs []config.CompressConfig) []CompressOption {
function Reset (line 136) | func Reset(configs []config.CompressConfig) {
function Get (line 141) | func Get(name string) *compressSrv {
FILE: compress/compress_test.go
function TestConvertConfig (line 16) | func TestConvertConfig(t *testing.T) {
function TestCompressLevel (line 37) | func TestCompressLevel(t *testing.T) {
function TestCompressList (line 51) | func TestCompressList(t *testing.T) {
function TestDecompress (line 97) | func TestDecompress(t *testing.T) {
FILE: compress/gzip.go
function doGunzip (line 32) | func doGunzip(buf []byte) ([]byte, error) {
function gzipFn (line 41) | func gzipFn(buf []byte, level int) (*bytes.Buffer, error) {
function doGzip (line 59) | func doGzip(buf []byte, level int) ([]byte, error) {
FILE: compress/gzip_test.go
function TestDoGzip (line 9) | func TestDoGzip(t *testing.T) {
function TestDoGunzip (line 41) | func TestDoGunzip(t *testing.T) {
FILE: compress/lz4.go
function doLZ4Encode (line 29) | func doLZ4Encode(data []byte, level int) ([]byte, error) {
function doLZ4Decode (line 39) | func doLZ4Decode(buf []byte) ([]byte, error) {
FILE: compress/lz4_test.go
function TestDoLZ4Decode (line 9) | func TestDoLZ4Decode(t *testing.T) {
FILE: compress/snappy.go
function doSnappyEncode (line 29) | func doSnappyEncode(data []byte) []byte {
function doSnappyDecode (line 35) | func doSnappyDecode(buf []byte) ([]byte, error) {
FILE: compress/snappy_test.go
function TestDoSnappyDecode (line 9) | func TestDoSnappyDecode(t *testing.T) {
FILE: compress/zstd.go
function doZSTDEncode (line 29) | func doZSTDEncode(data []byte, level int) ([]byte, error) {
function doZSTDDecode (line 41) | func doZSTDDecode(buf []byte) ([]byte, error) {
FILE: compress/zstd_test.go
function TestDoZSTDDecode (line 9) | func TestDoZSTDDecode(t *testing.T) {
FILE: config/config.go
type Client (line 37) | type Client interface
type OnChange (line 47) | type OnChange
type PikeConfig (line 50) | type PikeConfig struct
method Validate (line 165) | func (c *PikeConfig) Validate() error {
method GetAdminConfig (line 221) | func (p *PikeConfig) GetAdminConfig() AdminConfig {
type AdminConfig (line 63) | type AdminConfig struct
type CompressConfig (line 69) | type CompressConfig struct
type CacheConfig (line 75) | type CacheConfig struct
type UpstreamServerConfig (line 83) | type UpstreamServerConfig struct
type UpstreamConfig (line 90) | type UpstreamConfig struct
type LocationConfig (line 100) | type LocationConfig struct
type ServerConfig (line 113) | type ServerConfig struct
function InitDefaultClient (line 137) | func InitDefaultClient(url string) (err error) {
function Read (line 226) | func Read() (config *PikeConfig, err error) {
function Write (line 241) | func Write(config *PikeConfig) (err error) {
function Close (line 255) | func Close() (err error) {
function Watch (line 264) | func Watch(onChange OnChange) {
FILE: config/config_test.go
function TestValidate (line 34) | func TestValidate(t *testing.T) {
function TestInitDefaultClient (line 197) | func TestInitDefaultClient(t *testing.T) {
function TestReadWriteConfig (line 211) | func TestReadWriteConfig(t *testing.T) {
FILE: config/etcd_client.go
type etcdClient (line 38) | type etcdClient struct
method context (line 86) | func (ec *etcdClient) context() (context.Context, context.CancelFunc) {
method Get (line 95) | func (ec *etcdClient) Get() (data []byte, err error) {
method Set (line 111) | func (ec *etcdClient) Set(data []byte) (err error) {
method Watch (line 119) | func (ec *etcdClient) Watch(onChange OnChange) {
method Close (line 128) | func (ec *etcdClient) Close() error {
constant defaultEtcdTimeout (line 45) | defaultEtcdTimeout = 5 * time.Second
function NewEtcdClient (line 49) | func NewEtcdClient(uri string) (client *etcdClient, err error) {
FILE: config/etcd_client_test.go
function TestEtcdClient (line 31) | func TestEtcdClient(t *testing.T) {
FILE: config/file_client.go
type fileClient (line 37) | type fileClient struct
method Get (line 65) | func (fc *fileClient) Get() (data []byte, err error) {
method Set (line 70) | func (fc *fileClient) Set(data []byte) (err error) {
method Watch (line 75) | func (fc *fileClient) Watch(onChange OnChange) {
method Close (line 109) | func (fc *fileClient) Close() error {
constant defaultPerm (line 42) | defaultPerm os.FileMode = 0600
function NewFileClient (line 45) | func NewFileClient(file string) (client *fileClient, err error) {
FILE: config/file_client_test.go
function TestFileClient (line 32) | func TestFileClient(t *testing.T) {
FILE: config/validate.go
function init (line 39) | func init() {
function toString (line 110) | func toString(fl validator.FieldLevel) (string, bool) {
function contains (line 118) | func contains(arr []string, str string) bool {
function addValidate (line 129) | func addValidate(tag string, fn validator.Func, args ...bool) {
function addAlias (line 136) | func addAlias(alias, tags string) {
FILE: location/location.go
type Location (line 46) | type Location struct
method Match (line 135) | func (l *Location) Match(host, url string) bool {
method mergeHeader (line 163) | func (l *Location) mergeHeader(dst, src http.Header) {
method AddRequestHeader (line 172) | func (l *Location) AddRequestHeader(header http.Header) {
method AddResponseHeader (line 177) | func (l *Location) AddResponseHeader(header http.Header) {
method ShouldModifyQuery (line 182) | func (l *Location) ShouldModifyQuery() bool {
method AddQuery (line 187) | func (l *Location) AddQuery(req *http.Request) {
method getPriority (line 197) | func (l *Location) getPriority() int {
type rewriteRegexp (line 60) | type rewriteRegexp struct
type Rewriter (line 65) | type Rewriter
type Locations (line 68) | type Locations struct
method Set (line 224) | func (ls *Locations) Set(locations []Location) {
method GetLocations (line 244) | func (ls *Locations) GetLocations() []*Location {
method Get (line 252) | func (ls *Locations) Get(host, url string, names ...string) *Location {
function captureTokens (line 75) | func captureTokens(pattern *regexp.Regexp, input string) *strings.Replac...
function generateURLRewriter (line 91) | func generateURLRewriter(arr []string) Rewriter {
function NewLocations (line 215) | func NewLocations(opts ...Location) *Locations {
function enhanceGetValue (line 265) | func enhanceGetValue(key string) string {
function convertConfigs (line 272) | func convertConfigs(configs []config.LocationConfig) []Location {
function Reset (line 316) | func Reset(configs []config.LocationConfig) {
function Get (line 321) | func Get(host, url string, names ...string) *Location {
FILE: location/location_test.go
function TestLocation (line 17) | func TestLocation(t *testing.T) {
function TestLocations (line 123) | func TestLocations(t *testing.T) {
function TestConvertConfig (line 157) | func TestConvertConfig(t *testing.T) {
function TestDefaultLocations (line 217) | func TestDefaultLocations(t *testing.T) {
function TestURLRewrite (line 239) | func TestURLRewrite(t *testing.T) {
function TestAddHeader (line 277) | func TestAddHeader(t *testing.T) {
FILE: log/log.go
function init (line 34) | func init() {
type LumberjackLogger (line 43) | type LumberjackLogger struct
method Sync (line 47) | func (ll *LumberjackLogger) Sync() error {
function newLumberJack (line 51) | func newLumberJack(u *url.URL) (zap.Sink, error) {
function newLoggerX (line 81) | func newLoggerX(outputPath string) *zap.Logger {
function SetOutputPath (line 108) | func SetOutputPath(outputPath string) {
function Default (line 113) | func Default() *zap.Logger {
FILE: main.go
function init (line 38) | func init() {
function doAlarm (line 64) | func doAlarm(category, message string) {
function update (line 86) | func update() (err error) {
function startAdminServer (line 115) | func startAdminServer(addr string) error {
function runCMD (line 128) | func runCMD() error {
function run (line 175) | func run() {
function isHelpCmd (line 196) | func isHelpCmd() bool {
function isDev (line 206) | func isDev() bool {
function main (line 210) | func main() {
FILE: schedule/schedule.go
function init (line 32) | func init() {
function cpuUsageStats (line 39) | func cpuUsageStats() {
FILE: server/admin.go
type AdminServerConfig (line 46) | type AdminServerConfig struct
type loginParams (line 52) | type loginParams struct
type userInfo (line 56) | type userInfo struct
type applicationInfo (line 61) | type applicationInfo struct
constant jwtCookie (line 73) | jwtCookie = "pike"
function sendFile (line 77) | func sendFile(c *elton.Context, file string) (err error) {
function getUserAccount (line 88) | func getUserAccount(c *elton.Context) string {
function newIsLoginHandler (line 96) | func newIsLoginHandler(user string) elton.Handler {
function newLoginHandler (line 106) | func newLoginHandler(ttlToken *jwt.TTLToken, account, password string) e...
function newUserMeHandler (line 129) | func newUserMeHandler(user string) elton.Handler {
function updateServerStatus (line 142) | func updateServerStatus(conf *config.PikeConfig) {
function getConfig (line 170) | func getConfig(c *elton.Context) (err error) {
function saveConfig (line 181) | func saveConfig(c *elton.Context) (err error) {
function getApplicationInfo (line 216) | func getApplicationInfo(c *elton.Context) (err error) {
function removeCache (line 241) | func removeCache(c *elton.Context) (err error) {
function StartAdminServer (line 253) | func StartAdminServer(config AdminServerConfig) (err error) {
FILE: server/cache.go
constant spaceByte (line 34) | spaceByte = byte(' ')
function requestIsPass (line 38) | func requestIsPass(req *http.Request) bool {
function getKey (line 45) | func getKey(req *http.Request) []byte {
function NewCache (line 75) | func NewCache(s *server) elton.Handler {
FILE: server/cache_test.go
function TestRequestIsPass (line 35) | func TestRequestIsPass(t *testing.T) {
function TestGetKey (line 44) | func TestGetKey(t *testing.T) {
function TestCacheMiddleware (line 51) | func TestCacheMiddleware(t *testing.T) {
FILE: server/proxy.go
function getCacheMaxAge (line 47) | func getCacheMaxAge(header http.Header) int {
function NewProxy (line 85) | func NewProxy(s *server) elton.Handler {
FILE: server/proxy_test.go
function TestGetCacheMaxAge (line 45) | func TestGetCacheMaxAge(t *testing.T) {
function TestProxyMiddleware (line 114) | func TestProxyMiddleware(t *testing.T) {
FILE: server/responder.go
function NewResponder (line 32) | func NewResponder() elton.Handler {
FILE: server/responder_test.go
function TestResponderMiddleware (line 34) | func TestResponderMiddleware(t *testing.T) {
FILE: server/server.go
type server (line 45) | type server struct
method Update (line 268) | func (s *server) Update(opt ServerOption) {
method GetCache (line 279) | func (s *server) GetCache() string {
method GetLocations (line 286) | func (s *server) GetLocations() []string {
method GetCompress (line 293) | func (s *server) GetCompress() (name string, minLength int, filter *re...
method Start (line 300) | func (s *server) Start(useGoRoutine bool) (err error) {
method Close (line 364) | func (s *server) Close() error {
method GetListenAddr (line 379) | func (s *server) GetListenAddr() string {
type servers (line 61) | type servers struct
method Start (line 179) | func (ss *servers) Start() (err error) {
method Reset (line 197) | func (ss *servers) Reset(opts []ServerOption) {
method Close (line 239) | func (ss *servers) Close() error {
method Get (line 255) | func (ss *servers) Get(name string) *server {
type ServerOption (line 64) | type ServerOption struct
constant statusKey (line 84) | statusKey = "_status"
constant httpRespKey (line 86) | httpRespKey = "_httpResp"
constant httpRespAgeKey (line 88) | httpRespAgeKey = "_httpRespAge"
constant httpCacheMaxAgeKey (line 90) | httpCacheMaxAgeKey = "_httpCacheMaxAge"
constant defaultCompressMinLength (line 93) | defaultCompressMinLength = 1024
constant headerAge (line 98) | headerAge = "Age"
constant headerCacheStatus (line 99) | headerCacheStatus = "X-Status"
function getCacheStatus (line 112) | func getCacheStatus(c *elton.Context) cache.Status {
function setCacheStatus (line 115) | func setCacheStatus(c *elton.Context, cacheStatus cache.Status) {
function getHTTPResp (line 119) | func getHTTPResp(c *elton.Context) *cache.HTTPResponse {
function setHTTPResp (line 130) | func setHTTPResp(c *elton.Context, resp *cache.HTTPResponse) {
function setHTTPRespAge (line 134) | func setHTTPRespAge(c *elton.Context, age int) {
function getHTTPRespAge (line 137) | func getHTTPRespAge(c *elton.Context) int {
function setHTTPCacheMaxAge (line 141) | func setHTTPCacheMaxAge(c *elton.Context, age int) {
function getHTTPCacheMaxAge (line 144) | func getHTTPCacheMaxAge(c *elton.Context) int {
function NewServer (line 149) | func NewServer(opt ServerOption) *server {
function NewServers (line 168) | func NewServers(opts []ServerOption) *servers {
function convertConfig (line 383) | func convertConfig(configs []config.ServerConfig) []ServerOption {
function Reset (line 406) | func Reset(configs []config.ServerConfig) {
function Get (line 411) | func Get(name string) *server {
function Start (line 416) | func Start() error {
function Close (line 421) | func Close() error {
FILE: server/server_test.go
function TestGetSetCacheStatus (line 35) | func TestGetSetCacheStatus(t *testing.T) {
function TestGetSetHTTPResp (line 43) | func TestGetSetHTTPResp(t *testing.T) {
function TestGetSetHTTPRespAge (line 52) | func TestGetSetHTTPRespAge(t *testing.T) {
function TestGetSetHTTPCacheMaxAge (line 61) | func TestGetSetHTTPCacheMaxAge(t *testing.T) {
function TestServer (line 70) | func TestServer(t *testing.T) {
function TestServers (line 108) | func TestServers(t *testing.T) {
function TestConvertConfig (line 145) | func TestConvertConfig(t *testing.T) {
function TestDefaultServers (line 175) | func TestDefaultServers(t *testing.T) {
FILE: store/badger.go
type badgerStore (line 36) | type badgerStore struct
method Get (line 83) | func (bs *badgerStore) Get(key []byte) (data []byte, err error) {
method Set (line 104) | func (bs *badgerStore) Set(key []byte, data []byte, ttl time.Duration)...
method Delete (line 113) | func (bs *badgerStore) Delete(key []byte) (err error) {
method Close (line 120) | func (bs *badgerStore) Close() error {
type badgerLogger (line 40) | type badgerLogger struct
method Errorf (line 42) | func (bl *badgerLogger) Errorf(format string, args ...interface{}) {
method Warningf (line 48) | func (bl *badgerLogger) Warningf(format string, args ...interface{}) {
method Infof (line 54) | func (bl *badgerLogger) Infof(format string, args ...interface{}) {
method Debugf (line 60) | func (bl *badgerLogger) Debugf(format string, args ...interface{}) {
function newBadgerStore (line 68) | func newBadgerStore(path string) (Store, error) {
FILE: store/badger_test.go
function TestBadgerStore (line 33) | func TestBadgerStore(t *testing.T) {
FILE: store/mongo.go
constant defaultMongoCacheColletion (line 36) | defaultMongoCacheColletion = "caches"
constant defaultMongoDatabase (line 37) | defaultMongoDatabase = "pike"
type mongoStore (line 39) | type mongoStore struct
method collection (line 122) | func (ms *mongoStore) collection() *mongo.Collection {
method Get (line 127) | func (ms *mongoStore) Get(key []byte) (data []byte, err error) {
method Set (line 145) | func (ms *mongoStore) Set(key []byte, data []byte, ttl time.Duration) ...
method Delete (line 167) | func (ms *mongoStore) Delete(key []byte) (err error) {
method Close (line 180) | func (ms *mongoStore) Close() error {
type mongoCache (line 45) | type mongoCache struct
function fillMongoStoreOptions (line 51) | func fillMongoStoreOptions(connectionURI string, ms *mongoStore) {
function newMongoStore (line 69) | func newMongoStore(connectionURI string) (store Store, err error) {
FILE: store/mongo_test.go
function TestFillMongoStoreOptions (line 32) | func TestFillMongoStoreOptions(t *testing.T) {
function TestNewMongoStore (line 51) | func TestNewMongoStore(t *testing.T) {
FILE: store/redis.go
type redisStore (line 38) | type redisStore struct
method getKey (line 100) | func (rs *redisStore) getKey(key []byte) string {
method Get (line 105) | func (rs *redisStore) Get(key []byte) (data []byte, err error) {
method Set (line 121) | func (rs *redisStore) Set(key []byte, data []byte, ttl time.Duration) ...
method Delete (line 130) | func (rs *redisStore) Delete(key []byte) (err error) {
method Close (line 139) | func (rs *redisStore) Close() error {
type redisLogger (line 46) | type redisLogger struct
method Printf (line 48) | func (rl *redisLogger) Printf(ctx context.Context, format string, v .....
function init (line 54) | func init() {
function newRedisStore (line 58) | func newRedisStore(connectionURI string) (store Store, err error) {
FILE: store/redis_test.go
function TestNewRedisStore (line 32) | func TestNewRedisStore(t *testing.T) {
FILE: store/store.go
type Store (line 36) | type Store interface
function NewStore (line 54) | func NewStore(storeURL string) (store Store, err error) {
function GetStore (line 95) | func GetStore(storeURL string) Store {
function Close (line 108) | func Close() error {
FILE: store/store_test.go
function TestNewStrore (line 31) | func TestNewStrore(t *testing.T) {
FILE: test/main.go
function httpGet (line 14) | func httpGet(url string) (data []byte, err error) {
function main (line 27) | func main() {
FILE: upstream/upstream.go
type UpstreamServerConfig (line 47) | type UpstreamServerConfig struct
type UpstreamServerStatus (line 53) | type UpstreamServerStatus struct
type UpstreamServerOption (line 57) | type UpstreamServerOption struct
type upstreamServer (line 69) | type upstreamServer struct
method Destroy (line 245) | func (u *upstreamServer) Destroy() {
method GetServerStatusList (line 251) | func (u *upstreamServer) GetServerStatusList() []UpstreamServerStatus {
type upstreamServers (line 75) | type upstreamServers struct
method Reset (line 199) | func (us *upstreamServers) Reset(opts []UpstreamServerOption) {
method Get (line 232) | func (us *upstreamServers) Get(name string) *upstreamServer {
type StatusInfo (line 78) | type StatusInfo struct
type OnStatus (line 84) | type OnStatus
function newTransport (line 96) | func newTransport(h2c bool) http.RoundTripper {
function newTargetPicker (line 126) | func newTargetPicker(uh *us.HTTP) middleware.ProxyTargetPicker {
function newProxyMid (line 144) | func newProxyMid(opt UpstreamServerOption, uh *us.HTTP) elton.Handler {
function NewUpstreamServer (line 152) | func NewUpstreamServer(opt UpstreamServerOption) *upstreamServer {
function NewUpstreamServers (line 188) | func NewUpstreamServers(opts []UpstreamServerOption) *upstreamServers {
function Get (line 271) | func Get(name string) *upstreamServer {
function onStatus (line 275) | func onStatus(si StatusInfo) {
function convertConfigs (line 283) | func convertConfigs(configs []config.UpstreamConfig, fn OnStatus) []Upst...
function Reset (line 307) | func Reset(configs []config.UpstreamConfig) {
function ResetWithOnStats (line 312) | func ResetWithOnStats(configs []config.UpstreamConfig, fn OnStatus) {
FILE: upstream/upstream_test.go
function TestNewTransport (line 14) | func TestNewTransport(t *testing.T) {
function TestNewTargetPicker (line 30) | func TestNewTargetPicker(t *testing.T) {
function TestUpstreamServer (line 50) | func TestUpstreamServer(t *testing.T) {
function TestUpstreamServers (line 82) | func TestUpstreamServers(t *testing.T) {
function TestConvertConfig (line 118) | func TestConvertConfig(t *testing.T) {
function TestDefaultUpstreamServers (line 156) | func TestDefaultUpstreamServers(t *testing.T) {
FILE: util/util.go
constant errCategory (line 31) | errCategory = "pike"
type DeleteMatch (line 33) | type DeleteMatch
function MapDelete (line 36) | func MapDelete(m *sync.Map, match DeleteMatch) []interface{} {
function NewError (line 60) | func NewError(message string, statusCode int) error {
FILE: util/util_test.go
function TestMapDelete (line 33) | func TestMapDelete(t *testing.T) {
function TestNewError (line 52) | func TestNewError(t *testing.T) {
FILE: web/assets/packages/fluttertoast/assets/toastify.js
function i (line 14) | function i(t,o){return o.offset[t]?isNaN(o.offset[t])?o.offset[t]:o.offs...
function s (line 14) | function s(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&t.class...
FILE: web/flutter_service_worker.js
constant MANIFEST (line 2) | const MANIFEST = 'flutter-app-manifest';
constant TEMP (line 3) | const TEMP = 'flutter-temp-cache';
constant CACHE_NAME (line 4) | const CACHE_NAME = 'flutter-app-cache';
constant RESOURCES (line 5) | const RESOURCES = {
constant CORE (line 26) | const CORE = [
function downloadOffline (line 154) | async function downloadOffline() {
function onlineFirst (line 175) | function onlineFirst(event) {
FILE: web/main.dart.js
function copyProperties (line 1) | function copyProperties(a,b){var s=Object.keys(a)
function mixinProperties (line 3) | function mixinProperties(a,b){var s=Object.keys(a)
function setFunctionNamesIfNecessary (line 12) | function setFunctionNamesIfNecessary(a){function t(){};if(typeof t.name=...
function inherit (line 17) | function inherit(a,b){a.prototype.constructor=a
function inheritMany (line 22) | function inheritMany(a,b){for(var s=0;s<b.length;s++)inherit(b[s],a)}
function mixin (line 22) | function mixin(a,b){mixinProperties(b.prototype,a.prototype)
function lazyOld (line 23) | function lazyOld(a,b,c,d){var s=a
function lazy (line 30) | function lazy(a,b,c,d){var s=a
function lazyFinal (line 34) | function lazyFinal(a,b,c,d){var s=a
function makeConstList (line 39) | function makeConstList(a){a.immutable$list=Array
function convertToFastObject (line 41) | function convertToFastObject(a){function t(){}t.prototype=a
function convertAllToFastObject (line 43) | function convertAllToFastObject(a){for(var s=0;s<a.length;++s)convertToF...
function tearOffGetter (line 44) | function tearOffGetter(a,b,c,d,e){return e?new Function("funcs","applyTr...
function tearOff (line 44) | function tearOff(a,b,c,d,e,f){var s=null
function installTearOff (line 47) | function installTearOff(a,b,c,d,e,f,g,h,i,j){var s=[]
function installStaticTearOff (line 60) | function installStaticTearOff(a,b,c,d,e,f,g,h){return installTearOff(a,b...
function installInstanceTearOff (line 60) | function installInstanceTearOff(a,b,c,d,e,f,g,h,i){return installTearOff...
function setOrUpdateInterceptorsByTag (line 60) | function setOrUpdateInterceptorsByTag(a){var s=v.interceptorsByTag
function setOrUpdateLeafTags (line 62) | function setOrUpdateLeafTags(a){var s=v.leafTags
function updateTypes (line 64) | function updateTypes(a){var s=v.types
function updateHolder (line 67) | function updateHolder(a,b){copyProperties(b,a)
function initializeDeferredHunk (line 70) | function initializeDeferredHunk(a){x=v.types.length
function getGlobalFromName (line 71) | function getGlobalFromName(a){for(var s=0;s<w.length;s++){if(w[s]==C)con...
function getTag (line 72219) | function getTag(o) {
function getUnknownTag (line 72223) | function getUnknownTag(object, tag) {
function getUnknownTagGenericBrowser (line 72230) | function getUnknownTagGenericBrowser(object, tag) {
function prototypeForTag (line 72234) | function prototypeForTag(tag) {
function discriminator (line 72241) | function discriminator(tag) { return null; }
function confirm (line 72255) | function confirm(p) {
function getTagFixed (line 72270) | function getTagFixed(o) {
function prototypeForTagFixed (line 72278) | function prototypeForTagFixed(tag) {
function getTagFirefox (line 72296) | function getTagFirefox(o) {
function getTagIE (line 72314) | function getTagIE(o) {
function prototypeForTagIE (line 72323) | function prototypeForTagIE(tag) {
function onLoad (line 74610) | function onLoad(b){for(var q=0;q<s.length;++q)s[q].removeEventListener("...
Condensed preview — 96 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,313K chars).
[
{
"path": ".air.toml",
"chars": 1419,
"preview": "# Config file for [Air](https://github.com/cosmtrek/air) in TOML format\n\n# Working directory\n# . or absolute path, pleas"
},
{
"path": ".github/workflows/build.yml",
"chars": 1495,
"preview": "name: build on tag\n\non:\n push:\n tags:\n - 'v*.*.*'\n\njobs:\n docker:\n runs-on: ubuntu-latest\n name: Build\n "
},
{
"path": ".github/workflows/test.yml",
"chars": 2003,
"preview": "name: lint and test \n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n test:\n n"
},
{
"path": ".gitignore",
"chars": 317,
"preview": "vendor\ntmp\n*.out\n*.log\npike\npike-*\npackrd\n*-packr.go\n\n# See https://help.github.com/articles/ignoring-files/ for more ab"
},
{
"path": ".goreleaser.yml",
"chars": 635,
"preview": "# This is an example .goreleaser.yml file with some sane defaults.\n# Make sure to check the documentation at http://gore"
},
{
"path": "Dockerfile",
"chars": 550,
"preview": "FROM golang:1.16-alpine as builder\n\nCOPY ./ /pike \n\nRUN apk update \\\n && apk add git make \\\n && cd /pike \\\n && env \\\n"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2021 Tree Xie\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "Makefile",
"chars": 739,
"preview": ".PHONY: default test test-cover dev hooks\n\n# for dev\ndev:\n\tair -c .air.toml\t\n\n# for test\ntest:\n\tgo test -race -cover ./."
},
{
"path": "README.md",
"chars": 1445,
"preview": "# pike\n\n此项目已不再维护,建议使用基于pingora的[pingap](https://github.com/vicanso/pingap)项目。\n\n[\n* [模块](./docs/modules.md)\n* [缓存处理](./docs/cache-handler.md)\n* [响应处理](./docs/respons"
},
{
"path": "app/app.go",
"chars": 4131,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "app/app_test.go",
"chars": 565,
"preview": "package app\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSetSetBuildInfo(t *testin"
},
{
"path": "asset/asset.go",
"chars": 1366,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/cache.go",
"chars": 3135,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/cache_test.go",
"chars": 2293,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/dispatcher.go",
"chars": 5839,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/dispatcher_test.go",
"chars": 2745,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/http_cache.go",
"chars": 7444,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/http_cache_test.go",
"chars": 3220,
"preview": "package cache\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCacheStatusString("
},
{
"path": "cache/http_response.go",
"chars": 8585,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "cache/http_response_test.go",
"chars": 10405,
"preview": "package cache\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/"
},
{
"path": "cache/memhash.go",
"chars": 1441,
"preview": "// copy from https://github.com/dgraph-io/ristretto/blob/master/z/rtutil.go\n\npackage cache\n\nimport (\n\t\"unsafe\"\n)\n\n// Nan"
},
{
"path": "cache/memhash_test.go",
"chars": 329,
"preview": "package cache\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMemHash(t *testing.T) {\n\tassert :="
},
{
"path": "compress/brotli.go",
"chars": 1926,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "compress/brotli_test.go",
"chars": 1020,
"preview": "package compress\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDoBrotli(t *testing.T) {\n\tasser"
},
{
"path": "compress/compress.go",
"chars": 5436,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "compress/compress_test.go",
"chars": 4172,
"preview": "package compress\n\nimport (\n\t\"compress/gzip\"\n\t\"testing\"\n\n\t\"github.com/andybalholm/brotli\"\n\t\"github.com/stretchr/testify/a"
},
{
"path": "compress/gzip.go",
"chars": 1921,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "compress/gzip_test.go",
"chars": 1001,
"preview": "package compress\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDoGzip(t *testing.T) {\n\tassert "
},
{
"path": "compress/lz4.go",
"chars": 1576,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "compress/lz4_test.go",
"chars": 359,
"preview": "package compress\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDoLZ4Decode(t *testing.T) {\n\tas"
},
{
"path": "compress/snappy.go",
"chars": 1381,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "compress/snappy_test.go",
"chars": 326,
"preview": "package compress\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDoSnappyDecode(t *testing.T) {\n"
},
{
"path": "compress/zstd.go",
"chars": 1694,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "compress/zstd_test.go",
"chars": 345,
"preview": "package compress\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDoZSTDDecode(t *testing.T) {\n\ta"
},
{
"path": "config/config.go",
"chars": 9517,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "config/config_test.go",
"chars": 4769,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "config/etcd_client.go",
"chars": 3190,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "config/etcd_client_test.go",
"chars": 1681,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "config/file_client.go",
"chars": 2711,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "config/file_client_test.go",
"chars": 1670,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "config/validate.go",
"chars": 3428,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "docs/alarm.md",
"chars": 390,
"preview": "---\ndescription: 告警回调,通过配置相应的回调地址可及时获知程序异常\n---\n\n当程序出现异常时,如更新配置失败、upstream节点异常等,若启动指定了告警的回调地址,程序则会以POST的形式调用告警地址,内容如下: \n\n"
},
{
"path": "docs/cache-handler.md",
"chars": 1151,
"preview": "---\ndescription: 缓存处理\n---\n\nHTTP缓存使用了LRU缓存,能提供高效的缓存读取能力及有效控制缓存过大。为了减少缓存中锁的影响,使用了128个LRU组成缓存桶,每次根据hash选择对应的LRU,提升性能。需要注意,缓"
},
{
"path": "docs/error.md",
"chars": 572,
"preview": "---\ndescription: 程序处理异常时的各出错信息\n---\n\npike程序处理出错时,均返回category: \"pike\"的出错信息,可根据此分类判断是否系统错误,主要的错误如下:\n\n- `ErrInvalidResponse`"
},
{
"path": "docs/flow.drawio",
"chars": 3056,
"preview": "<mxfile host=\"Electron\" modified=\"2020-11-25T06:10:42.289Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/5"
},
{
"path": "docs/modules.drawio",
"chars": 2111,
"preview": "<mxfile host=\"Electron\" modified=\"2020-11-30T01:27:25.250Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/5"
},
{
"path": "docs/modules.md",
"chars": 2012,
"preview": "---\ndescription: 程序中的主要模块\n---\n\nPike有6个主要模块,下面对这些模块一一介绍。\n\n<p align=\"center\">\n<img src=\"./images/modules.png\"/>\n</p>\n\n## C"
},
{
"path": "docs/performance.md",
"chars": 1800,
"preview": "---\ndescription: 性能测试\n---\n\n测试机器:8U 8GB内存\n测试数据:数据原始长度约为140KB\n测试场景:客户端支持gzip、 br以及不支持压缩三种场景,并发请求数设置为1000,测试时长为1分钟\n测试结论:当客户"
},
{
"path": "docs/questions.md",
"chars": 1297,
"preview": "---\ndescription: 对于pike的各类常见疑问\n---\n\n## 何时需要配置缓存持久化\n\npike主要是用于缓存热点接口(可缓存),设计上保证了当缓存不存在或过期时,相同的请求只能有一个转发至后端。缓存也建议使用短缓存(不超过"
},
{
"path": "docs/response.drawio",
"chars": 2832,
"preview": "<mxfile host=\"Electron\" modified=\"2020-12-06T00:45:03.026Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/5"
},
{
"path": "docs/response.md",
"chars": 922,
"preview": "---\ndescription: 请求响应处理\n---\n\n请求响应的处理主要分两步,一是从upstream中获取响应(或从缓存中),二是根据响应与客户端选择符合的响应数据。HTTP响应数据主要有以下字段:\n\n- `GzipBody` gzi"
},
{
"path": "docs/start.md",
"chars": 6362,
"preview": "---\ndescription: 如何开始使用pike\n---\n\nPike是纯go的项目,可以使用各平台的执行文件启动或者已打包好的docker镜像(vicanso/pike),配置信息支持保存在文件或etcd中,生产环境中建议使用etcd"
},
{
"path": "entrypoint.sh",
"chars": 82,
"preview": "#!/bin/sh\nset -e\n\nif [ \"${1:0:1}\" = '-' ]; then\n set -- pike \"$@\"\nfi\n\nexec \"$@\""
},
{
"path": "go.mod",
"chars": 1341,
"preview": "module github.com/vicanso/pike\n\ngo 1.16\n\nreplace google.golang.org/grpc => google.golang.org/grpc v1.26.0\n\nreplace githu"
},
{
"path": "go.sum",
"chars": 58687,
"preview": "cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1"
},
{
"path": "hooks/pre-commit",
"chars": 34,
"preview": "#!/bin/sh\n#\nmake lint && make test"
},
{
"path": "location/location.go",
"chars": 7511,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "location/location_test.go",
"chars": 5755,
"preview": "package location\n\nimport (\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n"
},
{
"path": "log/log.go",
"chars": 2769,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "main.go",
"chars": 5020,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/spf13/cobra\""
},
{
"path": "pike.yml",
"chars": 864,
"preview": "version: 4.0.4\nadmin:\n user: vicanso\n password: z6+5xIgzATEPuK5T8LL8/SLWI7HhZdm5OJ445xaHsX0=\ncompresses:\n- name: compr"
},
{
"path": "schedule/schedule.go",
"chars": 1526,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/admin.go",
"chars": 8822,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/cache.go",
"chars": 3393,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/cache_test.go",
"chars": 3407,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/proxy.go",
"chars": 5937,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/proxy_test.go",
"chars": 6956,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/responder.go",
"chars": 1816,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/responder_test.go",
"chars": 2225,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/server.go",
"chars": 10044,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "server/server_test.go",
"chars": 5379,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/badger.go",
"chars": 3416,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/badger_test.go",
"chars": 1673,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/mongo.go",
"chars": 4563,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/mongo_test.go",
"chars": 2180,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/redis.go",
"chars": 3655,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/redis_test.go",
"chars": 2213,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/store.go",
"chars": 2937,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "store/store_test.go",
"chars": 1489,
"preview": "// MIT License\n\n// Copyright (c) 2021 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "test/main.go",
"chars": 947,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/vicanso/elton\"\n\t\"golang.org/x/net/http2\"\n"
},
{
"path": "upstream/upstream.go",
"chars": 8068,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "upstream/upstream_test.go",
"chars": 3738,
"preview": "package upstream\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/vicanso/elton\"\n\t\"g"
},
{
"path": "util/util.go",
"chars": 1880,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "util/util_test.go",
"chars": 1880,
"preview": "// MIT License\n\n// Copyright (c) 2020 Tree Xie\n\n// Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "web/assets/AssetManifest.json",
"chars": 325,
"preview": "{\"images/logo.png\":[\"images/logo.png\"],\"packages/cupertino_icons/assets/CupertinoIcons.ttf\":[\"packages/cupertino_icons/a"
},
{
"path": "web/assets/FontManifest.json",
"chars": 208,
"preview": "[{\"family\":\"MaterialIcons\",\"fonts\":[{\"asset\":\"fonts/MaterialIcons-Regular.otf\"}]},{\"family\":\"packages/cupertino_icons/Cu"
},
{
"path": "web/assets/NOTICES",
"chars": 779990,
"preview": "StackWalker\n\nCopyright (c) 2005-2009, Jochen Kalmbach\nAll rights reserved.\n\nRedistribution and use in source and binary "
},
{
"path": "web/assets/packages/fluttertoast/assets/toastify.css",
"chars": 1434,
"preview": "/**\n * Minified by jsDelivr using clean-css v4.2.3.\n * Original file: /npm/toastify-js@1.9.3/src/toastify.css\n *\n * Do N"
},
{
"path": "web/assets/packages/fluttertoast/assets/toastify.js",
"chars": 5366,
"preview": "/**\n * Minified by jsDelivr using Terser v5.3.0.\n * Original file: /npm/toastify-js@1.9.3/src/toastify.js\n *\n * Do NOT u"
},
{
"path": "web/flutter_service_worker.js",
"chars": 6986,
"preview": "'use strict';\nconst MANIFEST = 'flutter-app-manifest';\nconst TEMP = 'flutter-temp-cache';\nconst CACHE_NAME = 'flutter-ap"
},
{
"path": "web/index.html",
"chars": 1116,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n <meta n"
},
{
"path": "web/main.dart.js",
"chars": 2033812,
"preview": "(function dartProgram(){function copyProperties(a,b){var s=Object.keys(a)\nfor(var r=0;r<s.length;r++){var q=s[r]\nb[q]=a["
},
{
"path": "web/manifest.json",
"chars": 562,
"preview": "{\n \"name\": \"web\",\n \"short_name\": \"web\",\n \"start_url\": \".\",\n \"display\": \"standalone\",\n \"background_color\":"
},
{
"path": "web/version.json",
"chars": 55,
"preview": "{\"app_name\":\"web\",\"version\":\"1.0.0\",\"build_number\":\"1\"}"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the vicanso/pike GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 96 files (3.0 MB), approximately 787.0k tokens, and a symbol index with 434 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.