0

20 Commits

Author SHA1 Message Date
eleksir
f622330f41 Update README.md 2025-10-12 14:11:41 +03:00
eleksir
3ad60b9c69 Update dependencies 2025-10-12 13:55:23 +03:00
eleksir
897a2035d9 Remove TODO 2025-10-12 13:55:00 +03:00
eleksir
351695a7c5 Change logrus to slog, add missing cmd/*, update gitignore 2025-10-12 13:48:55 +03:00
eleksir
d527c4ab8f End transition to go project layout 2025-10-12 12:57:09 +03:00
eleksir
79840796f6 Move settings routines to separate internal module 2025-10-12 11:58:03 +03:00
eleksir
24507ed6b4 Upgrade versions of vendored libs 2025-04-06 15:38:55 +03:00
eleksir
484bfa9ba0 Add more debug, small optimizations
* Replace all time.Sleep() with <-time.NewTimer().C
* Add log.debug callback to all incomming network events
* Use slices.Contains() instead of iterating slices in for Loop
2025-03-30 19:49:55 +03:00
eleksir
8f8073ca3e More linter fixes, disable gci linter as it conflicts with goimports 2025-03-30 16:59:10 +03:00
eleksir
7bcc407857 Add golangci-lint config and fix linter warns 2025-03-30 16:19:20 +03:00
eleksir
8c24a1d8c6 Implement grabbing +B flag, connect command sequence fixes
* Implement i-am-bot, +B, flag grabbing.
* Move collections to internal packages.
* Postpone nickserv login sequence to 376 and 422 events, as it should be after motd message
* Postpone join channels to 376 and 422 events, as it should be after motd message
2025-03-30 15:41:46 +03:00
eleksir
4ca2fffb33 Upgrade versions of vendored libs 2025-02-09 15:41:24 +03:00
eleksir
d198741b37 Implement commands w п weather погода погодка погадка without arguments 2025-02-09 15:40:11 +03:00
eleksir
afe6e5384d Replace README with README.md 2024-08-03 21:35:56 +03:00
eleksir
b6f8cf9155 Upgrade versions of vendored libs 2024-08-03 21:26:21 +03:00
eleksir
9bac4fb257 Добавлена команда пословиться 2023-05-19 00:05:07 +03:00
eleksir
7958db9c0d Upgrade versions of vendored libs 2023-05-18 23:55:10 +03:00
eleksir
4869c146fe Replace deprecated ioutil.ReadFile() with os.ReadFile() 2023-05-18 23:52:47 +03:00
eleksir
b26387ea9b Add support for threadid field in redis json message protocol 2022-11-06 18:25:26 +03:00
eleksir
76a660ebed Upgrade versions of vendored libs 2022-11-04 16:21:02 +03:00
1212 changed files with 263214 additions and 38337 deletions

9
.gitignore vendored
View File

@@ -1,6 +1,3 @@
.idea/**
.vscode/**
.vstags
data/config.json
data/*_db
aleesa-irc-go
/data/config.json
/data/*_db
/aleesa-irc-go

83
.golangci.yml Normal file
View File

@@ -0,0 +1,83 @@
version: "2"
run:
go: "1.21"
modules-download-mode: readonly
tests: false
allow-parallel-runners: true
linters:
default: none
enable:
- asasalint
- asciicheck
- bodyclose
- containedctx
- contextcheck
- decorder
- durationcheck
- errcheck
- errchkjson
- errorlint
- exhaustive
- ginkgolinter
- gocheckcompilerdirectives
- gocritic
- godot
- gosec
- gosmopolitan
- govet
- grouper
- importas
- ineffassign
- ireturn
- makezero
- mirror
- nakedret
- nilerr
- nilnil
- nlreturn
- noctx
- nolintlint
- nonamedreturns
- predeclared
- promlinter
- reassign
- revive
- rowserrcheck
- staticcheck
- unconvert
- unused
- wastedassign
- whitespace
- wsl
settings:
gosec:
excludes:
# Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
- G404
# Не еби мозги. Сертификаты для локалхоста или для локальной сети могут быть невалидными, если ты сможешь
# выписать валидный серт для локалхоста, тебе миллионы разработчиков поставят золотой памятник. Просто потому
# что эти пидоры-безпасники всрали всё что только можно всрать своими дебильными политиками.
- G402
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- vendor$

12
DESIGN Normal file
View File

@@ -0,0 +1,12 @@
Для irc admin-модуля:
1. берём pebbledb как бэкэнд сохранения настроек.
2. имплементировать trusted-участников чятика для настройки бота под конкретный канальчик.
Есть два варианта:
- ручной список в конфиге, где прописывается owner бота и доверенные лица (опционально доверенных можно прописывать в pebbledb admin_db)
- доверенными являются op-ы
Первый вариант - довольно гибкий, но у нас всегда bot owner является доверенным. Не то чтобы это неправильно. Но с другой стороны можно сделать owner-а суперадмином, а другим админам дать возможность добавлять user-ов для своего канальчика в список доверенных. И запретить удалять себя из списка доверенных.
Второй вариант проще. И выглядит более привычным в текущих реалиях. Но есть нюанс - у нас нету простой возможности вынуть список участников вместе с флагами (предположительно). Кроме того, необходимо писать обработчк ивентов для всех изменений состояний юзеров (join/part и op/deop) для поддержания списка участников в актуальном состоянии.

View File

@@ -1,23 +1,88 @@
#!/usr/bin/env gmake -f
GOOPTS=CGO_ENABLED=0
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
MYNAME=aleesa-irc-go
BINARY=${MYNAME}
UNIX_BINARY=${MYNAME}
WINDOWS_BINARY=${MYNAME}.exe
RMCMD=rm -rf
# На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe
# не нужен
ifeq ($(OS),Windows_NT)
ifdef GOOS
ifeq ($(GOOS),windows)
BINARY=${WINDOWS_BINARY}
else # not ifeq ($(GOOS),windows)
BINARY=${MYNAME}
endif # ifeq ($(GOOS),windows)
else # not ifdef GOOS
BINARY=${WINDOWS_BINARY}
endif # ifdef GOOS
ifeq ($(SHELL), sh.exe)
RMCMD=DEL /Q /F
endif
endif
# Явно определяем символ новой строки, чтобы избежать неоднозначности на windows
define IFS
endef
all: clean build
build:
${GOOPTS} go build ${BUILDOPTS} -o aleesa-irc-go types.go globals.go collection.go irc-user-mode.go \
settings-db-util.go util.go msg-parsers.go aleesa-irc-go-lib.go main.go
ifeq ($(OS),Windows_NT)
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
ifeq ($(SHELL), sh.exe)
# # Vanilla cmd.exe / powershell.
SET "CGO_ENABLED=0"
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # git-bash
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # Some other shell.
# # TODO: handle it.
$(info "-- Dunno how to handle this shell: ${SHELL}")
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
else # not ($(OS),Windows_NT)
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
endif # ifeq ($(OS),Windows_NT)
clean:
go clean
ifeq ($(OS),Windows_NT)
ifeq ($(SHELL),sh.exe)
# # Vanilla cmd.exe / powershell.
if exist ${WINDOWS_BINARY} ${RMCMD} ${WINDOWS_BINARY}
if exist ${UNIX_BINARY} ${RMCMD} ${UNIX_BINARY}
else # not ifeq ($(SHELL),sh.exe)
${RMCMD} ./${WINDOWS_BINARY}
${RMCMD} ./${UNIX_BINARY}
endif # ifeq ($(SHELL),sh.exe)
else # not ifeq ($(OS),Windows_NT)
${RMCMD} ./${BINARY}
endif
wipe:
go clean
rm -rf go.{mod,sum}
prep:
go mod init main
go mod tidy -compat=1.18
upgrade:
ifeq ($(OS),Windows_NT)
ifeq ($(SHELL),sh.exe)
# # Vanilla cmd.exe / powershell.
if exist vendor DEL /F /S /Q vendor >nul
else # not ifeq ($(SHELL),sh.exe)
# # git-bash
$(RM) -r vendor
endif # ifeq ($(SHELL),sh.exe)
else # not ifeq ($(OS),Windows_NT)
$(RM) -r vendor
endif
go get -u ./...
go mod tidy
go mod vendor
# vim: set ft=make noet ai ts=4 sw=4 sts=4:

82
README
View File

@@ -1,82 +0,0 @@
aleesa-irc-go
-----------------------------------------------------------------------------------------------------------------------
Сервис для aleesa-bot, работающий с протоколом IRC. С основными сервисами бота работает по протоколу Redis pubsub.
Механика работы
-----------------------------------------------------------------------------------------------------------------------
Сервис подписан на pub-sub redis-канал irc. В этот канал пишут остальные сервисы aleesa-bot-а в ответ на запросы этого
сервиса.
Формат протокола общения по redis pub-sub - см README от aleesa-misc-go.
Как это собрать?
-----------------------------------------------------------------------------------------------------------------------
Понадобится golang версии 1.18 или более новая.
Сам процесс сборки простой, достаточно запустить:
make
и через некоторое время мы получаем бинарник aleesa-irc-go. Это и есть наше приложение.
Как это запустить?
-----------------------------------------------------------------------------------------------------------------------
Приложение тестировалось на alpine linux x86_64 3.16, но должно работать на любом (более свежем) linux-е.
После создания data/config.json из data/config_sample.json, можно запускать приложение.
Предполагается, что конфиг написан в формате human json (https://hjson.github.io), который допускает коментарии и
некоторые другие вольности в форматировании json-а. Конфиг ищется последовательно по следующим путям по принципу "кто
первый того и тапки":
~/.aleesa-irc-go.json
~/aleesa-irc-go.json
/etc/aleesa-irc-go.json
data/config.json
В каталоге contrib находится скрипт для alpine linux, системы инициализации openrc. Этот скрипт достаточно положить в
/etc/init.d/aleesa-irc-go. После чего скопировать бинарник в каталог /var/lib/aleesa-irc-go, положить конфиг в одну
из дефолтных локаций (тестировалась /var/lib/aleesa-irc-go/data/config.json) и после этого сервис запускается
командой:
service aleesa-irc-go start
В автозагрузку его можно прописать через команду:
rc-update add aleesa-irc-go default
Nota Bene
-----------------------------------------------------------------------------------------------------------------------
По механике, в процессе работы мы форкаем горутинки на каждое входящее сообщение, особо не контроллируя этот процесс.
Предполагается, что на это приложение не будет большой нагрузки - ограничения irc не должны дать возможности развить
достаточно большой message rate, чтобы об это споткнуться.
Формально, более удачным было бы иметь автоматический пул воркеров-горутинок и раскидывать им через channel-ы
сообщения. Внутри каждой горутинки можно было бы повиcнуть в бесконечном цикле на канале входящих сообщений и так и
работать. Но на практике оно того не стОит.
Nota Bene #2
-----------------------------------------------------------------------------------------------------------------------
Go не поддерживает системный вызов fork() из-за чего демонизация программ на гошке средствами самой гошки - это в
некотором роде проблема. Решается она с помощью утилиты daemonize (https://github.com/bmc/daemonize).
Соответствено, если для запуска этой программы предполагается использовать возможности демонизации, то можно
воспользоваться этой программой.
В alpine linux есть средства openrc для супервайзинга foreground-сервисов.
Nota Bene #3
-----------------------------------------------------------------------------------------------------------------------
К сожалению, в боте не подразумевается функциональность invite-ов. Причина банальна - слишком жёсткие ограничения у
многих irc-сетей на количество сообщений в единицу времени, а по механике в большинстве случаев ограничение работает не
client-client или client-channel, а глобально, client-server (за исключением случаев, когда на канале у бота есть +v
или +o), что позволяет абузить клиента, то есть в данном случае, бота.
Сообщения об invite-ах пишутся в лог с уровнем info.
Special Thanks
-----------------------------------------------------------------------------------------------------------------------
https://github.com/akyoto/cache за "простой" кэш, из которого я сделал collection.go

83
README.md Normal file
View File

@@ -0,0 +1,83 @@
# aleesa-irc-go
Сервис для aleesa-bot, работающий с протоколом IRC. С основными сервисами бота работает по протоколу Redis pubsub.
## Механика работы
Сервис подписан на pub-sub redis-канал irc. В этот канал пишут остальные сервисы aleesa-bot-а в ответ на запросы этого
сервиса.
Формат протокола общения по redis pub-sub - см. документацию в репозитории aleesa-doc.
## Как это собрать?
Понадобится компилятор go версии 1.25.1 или более новый.
Сам процесс сборки простой, достаточно запустить make или gmake, в зависимости от платформы:
```bash
make
```
и через некоторое время мы получаем бинарник **aleesa-irc-go**. Это и есть наше приложение.
## Как это запустить?
Приложение тестировалось на alpine linux x86_64 3.16, но должно работать на любом (более свежем) linux-е, windows-е,
freebsd, macos.
После создания **data/config.json** из **data/config_sample.json**, можно запускать приложение.
Предполагается, что конфиг написан в формате [human json](https://hjson.github.io), который допускает коментарии и
некоторые другие вольности в форматировании json-а. Конфиг ищется последовательно по следующим путям по принципу "кто
первый того и тапки":
```
~/.aleesa-irc-go.json
~/aleesa-irc-go.json
/etc/aleesa-irc-go.json
data/config.json
```
В каталоге contrib находится скрипт для alpine linux, системы инициализации openrc. Этот скрипт достаточно положить в
**/etc/init.d/aleesa-irc-go**. После чего скопировать бинарник в каталог **/var/lib/aleesa-irc-go**, положить конфиг в
одну из дефолтных локаций (тестировалась **/var/lib/aleesa-irc-go/data/config.json**) и после этого сервис запускается
командой:
```bash
service aleesa-irc-go start
```
В автозагрузку его можно прописать через команду:
```bash
rc-update add aleesa-irc-go default
```
## Nota Bene
Go не поддерживает системный вызов fork() из-за чего демонизация программ на гошке средствами самой гошки - это в
некотором роде проблема. Решается она с помощью утилиты [daemonize](https://github.com/bmc/daemonize).
Соответствено, если для запуска этой программы предполагается использовать возможности демонизации, то можно
воспользоваться этой программой.
В alpine linux есть средства openrc для супервайзинга foreground-сервисов.
Технически, можно было бы решить эту проблему fork()-а с помощью "github.com/sevlyar/go-daemon", но на текущий момент
она не стоит остро, поэтому решения не требуется.
## Nota Bene 2
К сожалению, в боте не подразумевается функциональность invite-ов. Причина банальна - слишком жёсткие ограничения у
многих irc-сетей на количество сообщений в единицу времени, а по механике в большинстве случаев ограничение работает не
client-client или client-channel, а глобально, client-server (за исключением случаев, когда на канале у бота есть +v
или +o), что позволяет абузить клиента, то есть в данном случае, бота.
Сообщения об invite-ах пишутся в лог с уровнем info.
## Special Thanks
https://github.com/akyoto/cache за "простой" кэш, из которого я сделал anycollection и boolcollection.

89
cmd/aleesa-irc-go/main.go Normal file
View File

@@ -0,0 +1,89 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"aleesa-irc-go/internal/irc"
"aleesa-irc-go/internal/log"
"aleesa-irc-go/internal/setting"
"github.com/go-redis/redis/v8"
)
// Производит некоторую инициализацию перед запуском main().
func init() {
irc.ReadConfig()
// Проинициализируем мапку MODE-ами пользователей произвольным значением и удалим его
var usersModes = map[string]map[string]map[string]bool{
"user": {
"mode": {
"v": true,
},
},
}
irc.UserMode.Set("channel", usersModes)
irc.UserMode.Delete("channel")
setting.DataDir = irc.Config.DataDir
}
// Собственно, какбэ "точка входа" - основная процедура в нашем боте.
func main() {
// Main context
var (
ctx = context.Background()
logfile *os.File
err error
)
// Откроем лог и скормим его логгеру
if irc.Config.Log != "" {
logfile, err = os.OpenFile(irc.Config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
log.Errorf("Unable to open log file %s: %s", irc.Config.Log, err)
os.Exit(1)
}
} else {
logfile = os.Stderr
}
log.Init(irc.Config.Loglevel, logfile)
// Иницализируем redis-клиента
irc.RedisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", irc.Config.Redis.Server, irc.Config.Redis.Port),
})
log.Debugf("Lazy connect() to redis at %s:%d", irc.Config.Redis.Server, irc.Config.Redis.Port)
irc.Subscriber = irc.RedisClient.Subscribe(ctx, irc.Config.Redis.MyChannel)
redisMsgChan := irc.Subscriber.Channel()
go irc.IrcClientRun()
go irc.IrcSend()
go irc.IrcSendUnrestricted()
// Самое время поставить траппер сигналов
signal.Notify(irc.SigChan,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
go irc.SigHandler()
// Обработчик событий от редиски
for msg := range redisMsgChan {
if irc.Shutdown {
continue
}
irc.RedisMsgParser(msg.Payload)
}
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

45
go.mod
View File

@@ -1,32 +1,41 @@
module aleesa-irc-go
go 1.18
go 1.25.1
require (
github.com/cockroachdb/errors v1.8.1
github.com/cockroachdb/pebble v0.0.0-20220729140737-245afda9c39f
github.com/cockroachdb/errors v1.12.0
github.com/cockroachdb/pebble v1.1.5
github.com/go-redis/redis/v8 v8.11.5
github.com/hjson/hjson-go v3.1.0+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/hjson/hjson-go v3.3.0+incompatible
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
)
require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect
github.com/cockroachdb/redact v1.0.8 // indirect
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/klauspost/compress v1.11.7 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/exp v0.0.0-20200513190911-00229845015e // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/text v0.3.6 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)

389
go.sum
View File

@@ -1,348 +1,137 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4=
github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM=
github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y=
github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/cockroachdb/pebble v0.0.0-20220729140737-245afda9c39f h1:ZGX53mhE1cb3CoICFir642DOR6JzSSkft0M9L806Wgo=
github.com/cockroachdb/pebble v0.0.0-20220729140737-245afda9c39f/go.mod h1:890yq1fUb9b6dGNwssgeUO5vQV9qfXnCPxAJhBQfXw0=
github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw=
github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM=
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.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 v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
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/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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
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/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk=
github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U=
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/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.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
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/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20200513190911-00229845015e h1:rMqLP+9XLy+LdbCXHjJHAmTfXCr93W7oruWA6Hq1Alc=
golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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-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=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
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/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,21 +1,21 @@
package main
package anycollection
import (
"sync"
)
// Collection это sync.Map-ка, хранящая в себе произвольные итемы
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
type Collection struct {
items sync.Map
close chan struct{}
}
// Item представляет собой интерфейс для хранения произвольных данных
// Item представляет собой интерфейс для хранения произвольных данных.
type item struct {
data interface{}
data any
}
// NewCollection создаёт инстанс коллекции
// NewCollection создаёт инстанс коллекции.
func NewCollection() *Collection {
c := &Collection{
close: make(chan struct{}),
@@ -24,8 +24,8 @@ func NewCollection() *Collection {
return c
}
// Get достаёт данные по заданному ключу из коллекции
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
// Get достаёт данные по заданному ключу из коллекции.
func (collection *Collection) Get(key any) (any, bool) {
obj, exists := collection.items.Load(key)
if !exists {
@@ -37,16 +37,16 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) {
return item.data, true
}
// Set сохраняет данные с заданным ключом в коллекцию
func (collection *Collection) Set(key interface{}, value interface{}) {
// Set сохраняет данные с заданным ключом в коллекцию.
func (collection *Collection) Set(key any, value any) {
collection.items.Store(key, item{
data: value,
})
}
// Range применяет функцию f ко всем ключам в коллекции
func (collection *Collection) Range(f func(key, value interface{}) bool) {
fn := func(key, value interface{}) bool {
// Range применяет функцию f ко всем ключам в коллекции.
func (collection *Collection) Range(f func(key, value any) bool) {
fn := func(key, value any) bool {
item := value.(item)
return f(key, item.data)
@@ -55,12 +55,12 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) {
collection.items.Range(fn)
}
// Delete удаляет ключ и значение из коллекции данных
func (collection *Collection) Delete(key interface{}) {
// Delete удаляет ключ и значение из коллекции данных.
func (collection *Collection) Delete(key any) {
collection.items.Delete(key)
}
// Close очищает и высвобождает ресурсы, занятые коллекцией
// Close очищает и высвобождает ресурсы, занятые коллекцией.
func (collection *Collection) Close() {
collection.close <- struct{}{}
collection.items = sync.Map{}

View File

@@ -0,0 +1,58 @@
package boolcollection
import (
"sync"
)
// Collection это sync.Map-ка, хранящая в себе итемы типа bool.
type Collection struct {
items sync.Map
close chan struct{}
}
// Item представляет собой интерфейс для хранения данных типа bool.
type item struct {
data bool
}
// NewCollection создаёт инстанс коллекции.
func NewCollection() *Collection {
c := &Collection{
close: make(chan struct{}),
}
return c
}
// Get достаёт данные по заданному ключу из коллекции.
func (collection *Collection) Get(key string) (bool, bool) {
obj, exists := collection.items.Load(key)
if !exists {
return false, false
}
item := obj.(item)
return item.data, true
}
// Set сохраняет данные с заданным ключом в коллекцию.
func (collection *Collection) Set(key string, value bool) {
collection.items.Store(key, item{
data: value,
})
}
// Delete удаляет ключ и значение из коллекции данных.
func (collection *Collection) Delete(key string) {
collection.items.Delete(key)
}
// Close очищает и высвобождает ресурсы, занятые коллекцией.
func (collection *Collection) Close() {
collection.close <- struct{}{}
collection.items = sync.Map{}
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,38 +1,40 @@
package main
package irc
import (
"crypto/tls"
"fmt"
"regexp"
"slices"
"strings"
"time"
log "github.com/sirupsen/logrus"
"aleesa-irc-go/internal/boolcollection"
"aleesa-irc-go/internal/log"
irc "github.com/thoj/go-ircevent"
)
// ircClientInit горутинка для работы с протоколом irc
func ircClientRun() {
for {
if shutdown {
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо
break
}
// Иницализируем irc-клиента
serverString := fmt.Sprintf("%s:%d", config.Irc.Server, config.Irc.Port)
// ircClientInit горутинка для работы с протоколом irc.
func IrcClientRun() {
for !Shutdown {
// Иницализируем irc-клиента.
// TODO: make use of capabilities: https://defs.ircdocs.horse/defs/clientcaps .
// TODO: make use of tags caps https://defs.ircdocs.horse/defs/tags .
serverString := fmt.Sprintf("%s:%d", Config.Irc.Server, Config.Irc.Port)
log.Debugf("Preparing to connect to %s", serverString)
log.Debugf("Using nick %s and username %s", config.Irc.Nick, config.Irc.User)
ircClient = irc.IRC(config.Irc.Nick, config.Irc.User)
ircClient.RealName = config.Irc.User
log.Debugf("Using nick %s and username %s", Config.Irc.Nick, Config.Irc.User)
ircClient = irc.IRC(Config.Irc.Nick, Config.Irc.User)
ircClient.RealName = Config.Irc.User
ircClient.Version = "Aleesa Bot v4.something"
if config.Irc.Ssl {
if Config.Irc.Ssl {
log.Debug("Force use ssl for connection")
ircClient.UseTLS = true
if !config.Irc.SslVerify {
if !Config.Irc.SslVerify {
log.Debug("Skip server certificate validation")
ircClient.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} else {
log.Debug("Force server certificate validation")
@@ -41,58 +43,97 @@ func ircClientRun() {
log.Debug("Skip ssl for connection")
}
if config.Irc.Sasl && config.Irc.Password != "" {
ircClient.SASLLogin = config.Irc.User
ircClient.SASLPassword = config.Irc.Password
if Config.Irc.Sasl && Config.Irc.Password != "" {
ircClient.SASLLogin = Config.Irc.User
ircClient.SASLPassword = Config.Irc.Password
ircClient.UseSASL = true
}
// Навесим коллбэков на некоторые ответы сервера на наши запросы
// Навесим коллбэков на некоторые ответы сервера на наши запросы.
// 001 RPL_WELCOME уже есть в github.com/thoj/go-ircevent/irc_callback.go.
// Сделаем уже что-то полезное! Motd нам уже прислали и теперь можно авторизоваться и джойниться
ircClient.AddCallback("004", func(e *irc.Event) {
// Из это строки мы можем узнать, какие флаги для user MODE и channem MODE можно навешивать.
// Как минимум, эта строка нам нужна, чтобы выяснить, можем ли мы взять себе +B, мы же бот :).
e.Connection.Lock()
// Формат строки с MODE-ами https://datatracker.ietf.org/doc/html/rfc2812#section-5.1 .
availableUserModes = boolcollection.NewCollection()
availableChanModes = boolcollection.NewCollection()
log.Debugf("Add %s to list of available user modes", e.Arguments[3])
for _, mode := range strings.Split(e.Arguments[3], "") {
availableUserModes.Set(mode, true)
}
availableUserModes.Set("announced", true)
log.Debugf("Add %s to list available channel modes", e.Arguments[4])
for _, mode := range strings.Split(e.Arguments[4], "") {
availableChanModes.Set(mode, true)
}
availableChanModes.Set("announced", true)
e.Connection.Unlock()
})
ircClient.AddCallback("005", func(e *irc.Event) {
// TODO: Make use of it. https://defs.ircdocs.horse/defs/isupport
})
ircClient.AddCallback("319", func(e *irc.Event) {
/* Это одна из строк с данными, прилетающая в ответ на запрос whois на определённого юзера
* Из этой строки нас интересует, на каких каналах пользователь op (то есть с префиксом @) или имеет voice
* (то есть с префиксом +) чтобы внести его в свою базу mode-ов.
*/
log.Debugf("319 RPL_WHOISCHANNELS, %s", e.Raw)
channelsWithModes := strings.Split(e.Arguments[2], " ")
dstNick := e.Arguments[1]
for _, channelWithMode := range channelsWithModes {
for _, mychannel := range config.Irc.Channels {
for _, mychannel := range Config.Irc.Channels {
tmp := regexp.MustCompile("#").Split(channelWithMode, 2)
channel := tmp[1]
modes := tmp[0]
if channel == mychannel {
// Для каждого канала формат строго 1 из 4-х: #channel | @#channel | +#channel | @+#channel
switch {
case modes == "@+":
switch modes {
case "@+":
userModeUpdateUser(channel, dstNick, "+o")
userModeUpdateUser(channel, dstNick, "+v")
case modes == "@":
case "@":
userModeUpdateUser(channel, dstNick, "+o")
case modes == "+":
case "+":
userModeUpdateUser(channel, dstNick, "+v")
default:
userModeUpdateUser(channel, dstNick, "-o")
userModeUpdateUser(channel, dstNick, "-v")
}
break
}
}
}
})
// 433 ERR_NICKNAMEINUSE уже есть в github.com/thoj/go-ircevent/irc_callback.go.
ircClient.AddCallback("353", func(e *irc.Event) {
// Это одна из строк данных, прилетающая в ответ на запрос names, на канале
log.Debugf("353 RPL_NAMREPLY, %s", e.Raw)
e.Connection.Lock()
namesString := e.Arguments[3]
channel := e.Arguments[2]
for _, mychannel := range config.Irc.Channels {
for _, mychannel := range Config.Irc.Channels {
if channel == mychannel {
for _, name := range strings.Split(namesString, " ") {
mode := name[:1]
var nick string
if mode == "@" || mode == "+" {
@@ -101,25 +142,58 @@ func ircClientRun() {
nick = name
}
switch {
case mode == "@":
switch mode {
case "@":
userModeUpdateUser(channel, nick, "+o")
case mode == "+":
case "+":
userModeUpdateUser(channel, nick, "+v")
default:
userModeUpdateUser(channel, nick, "-o")
userModeUpdateUser(channel, nick, "-v")
}
}
break
}
}
e.Connection.Unlock()
})
ircClient.AddCallback("352", func(e *irc.Event) {
// Относительно бесполезная для нас команда. Не возвращает канала (в случае с ngircd), так что узнать mode
// юзера на конкретном канале из этого невозможно. А остальные данные пока непонятно куда применять.
log.Warnf("352 RPL_WHOREPLY, %s", e.Raw)
// Если сервер не может прочитать MOTD, то он может вернуть 422 ERR_NOMOTD, тоде самое навесим и туда тоже.
ircClient.AddCallback("376", func(e *irc.Event) {
// TODO: make use of modes https://defs.ircdocs.horse/defs/usermodes
// Если у нас есть доступный +B возьмём его себе, мы же бот.
announced, _ := availableUserModes.Get("announced")
botFlag, _ := availableUserModes.Get("B")
if announced {
if botFlag {
log.Info("Grabbing +B flag to mark me as bot")
ircClient.Mode(ircClient.GetNick(), "+B")
// TODO: Wait for it.
} else {
log.Info("Skip +B flag, server does not support it, noone will know that I am bot")
}
} else {
// Этого по идее не должно быть.
log.Info("Skip +B flag, server did not announce modes (yet?), noone will know that I am bot")
}
if !Config.Irc.Sasl && Config.Irc.Password != "" && !nickIsUsed {
log.Info("Identifying via NickServ")
message := fmt.Sprintf("identify %s %s", Config.Irc.Nick, Config.Irc.Password)
imChan <- iMsg{ChatID: "NickServ", Text: message}
}
// TODO: wait until +R flag been set? could be implemented with waiting for channel message.
log.Debug("Trying to join to preconfigured channels")
for _, channel := range Config.Irc.Channels {
log.Infof("Joining to %s channel", channel)
ircClient.Join(channel)
}
// TODO: wait for join or join error.
})
// Навесим коллбэков на все возможные и невозможные error status code, которые мы можем получить и сдампим это
@@ -157,6 +231,39 @@ func ircClientRun() {
log.Errorf("421 ERR_UNKNOWNCOMMAND, %s", e.Raw)
})
// Аналогичный коллбэк висит на 376 RPL_ENDOFMOTD.
ircClient.AddCallback("422", func(e *irc.Event) {
// TODO: make use of modes https://defs.ircdocs.horse/defs/usermodes
// Если у нас есть доступный +B возьмём его себе, мы же бот.
botFlag, ok := availableUserModes.Get("B")
if ok && botFlag {
log.Info("Grabbing +B flag to mark me as bot")
ircClient.Mode(ircClient.GetNick(), "+B")
} else {
if ok {
log.Info("Skip +B flag, server does not support it, noone will know that I am bot")
} else {
log.Info("Skip +B flag, server did not announce modes (yet?), noone will know that I am bot")
}
}
if !Config.Irc.Sasl && Config.Irc.Password != "" && !nickIsUsed {
log.Info("Identifying via NickServ")
message := fmt.Sprintf("identify %s %s", Config.Irc.Nick, Config.Irc.Password)
imChan <- iMsg{ChatID: "NickServ", Text: message}
}
// TODO: wait until +R flag been set? could be implemented with waiting for channel message.
log.Debug("Trying to join to preconfigured channels")
for _, channel := range Config.Irc.Channels {
log.Infof("Joining to %s channel", channel)
ircClient.Join(channel)
}
})
ircClient.AddCallback("431", func(e *irc.Event) {
log.Errorf("431 ERR_NONICKNAMEGIVEN, %s", e.Raw)
})
@@ -167,9 +274,12 @@ func ircClientRun() {
ircClient.AddCallback("433", func(e *irc.Event) {
log.Errorf("433 ERR_NICKNAMEINUSE, %s", e.Raw)
nickIsUsed = true
time.Sleep(60 * time.Second)
ircClient.Nick(config.Irc.Nick)
<-time.NewTimer(60 * time.Second).C // 1 minute sleep.
ircClient.Nick(Config.Irc.Nick)
})
ircClient.AddCallback("436", func(e *irc.Event) {
@@ -179,6 +289,7 @@ func ircClientRun() {
log.Errorf("436 ERR_NICKCOLLISION, %s", e.Raw)
})
// 437: ERR_UNAVAILRESOURCE уже есть в github.com/thoj/go-ircevent/irc_callback.go.
ircClient.AddCallback("437", func(e *irc.Event) {
log.Errorf("437 ERR_UNAVAILRESOURCE, %s", e.Raw)
})
@@ -191,6 +302,16 @@ func ircClientRun() {
log.Warnf("442 ERR_NOTONCHANNEL, %s", e.Raw)
})
ircClient.AddCallback("443", func(e *irc.Event) {
// Returned when a client tries to invite a user to a channel they're already on.
log.Errorf("443 ERR_USERONCHANNEL, %s", e.Raw)
})
ircClient.AddCallback("446", func(e *irc.Event) {
// Returned by USERS when it has been disabled or not implemented.
log.Errorf("446 ERR_USERSDISABLED, %s", e.Raw)
})
ircClient.AddCallback("451", func(e *irc.Event) {
// Предполагается, что надо авторизоваться, перед тем как что-то делать на сервере
log.Errorf("451 ERR_NOTREGISTERED, %s", e.Raw)
@@ -213,18 +334,19 @@ func ircClientRun() {
log.Errorf("465 ERR_YOUREBANNEDCREEP, %s", e.Raw)
})
ircClient.AddCallback("467", func(e *irc.Event) {
log.Errorf("467 ERR_KEYSET, %s", e.Raw)
})
ircClient.AddCallback("471", func(e *irc.Event) {
// Это значит, что народу на канале максимальное количество. Будем пробовать присунуться раз в 30 сек.
channel := e.Arguments[1]
log.Warnf("471 ERR_CHANNELISFULL, %s", e.Raw)
time.Sleep(30 * time.Second)
<-time.NewTimer(30 * time.Second).C
// Проверяем, а должны ли мы быть заджоенныеми к указанному, каналу, а то вдруг нет?
for _, ircChan := range config.Irc.Channels {
if ircChan == channel {
ircClient.Join(channel)
break
}
if slices.Contains(Config.Irc.Channels, channel) {
ircClient.Join(channel)
}
})
@@ -237,14 +359,11 @@ func ircClientRun() {
// По завершении работ +i снимают.
channel := e.Arguments[1]
log.Errorf("473 ERR_INVITEONLYCHAN, %s", e.Raw)
time.Sleep(30 * time.Second)
<-time.NewTimer(30 * time.Second).C
// Проверяем, а должны ли мы быть заджоенныеми к указанному, каналу, а то вдруг нет?
for _, ircChan := range config.Irc.Channels {
if ircChan == channel {
ircClient.Join(channel)
break
}
// Проверяем, а должны ли мы быть заджоенными к указанному, каналу, а то вдруг нет?
if slices.Contains(Config.Irc.Channels, channel) {
ircClient.Join(channel)
}
})
@@ -254,18 +373,21 @@ func ircClientRun() {
log.Errorf("474 ERR_BANNEDFROMCHAN, %s", e.Raw)
// Если нас забанили, то информацию о MODE-ах пользователей мы теряем
time.Sleep(30 * time.Second)
<-time.NewTimer(30 * time.Second).C
// Вдруг, нас забанили, но какбэ не навсегда?
// TODO: вынести в настройки?
// Проверяем, а должны ли мы быть заджоенныеми к указанному, каналу, а то вдруг нет?
for _, ircChan := range config.Irc.Channels {
if ircChan == channel {
ircClient.Join(channel)
break
}
if slices.Contains(Config.Irc.Channels, channel) {
ircClient.Join(channel)
}
})
ircClient.AddCallback("475", func(e *irc.Event) {
log.Errorf("475 ERR_BADCHANNELKEY, %s", e.Raw)
})
ircClient.AddCallback("477", func(e *irc.Event) {
log.Errorf("477 ERR_NOCHANMODES, %s", e.Raw)
})
@@ -274,6 +396,10 @@ func ircClientRun() {
log.Errorf("478 ERR_BANLISTFULL, %s", e.Raw)
})
ircClient.AddCallback("481", func(e *irc.Event) {
log.Errorf("481 ERR_NOPRIVILEGES, %s", e.Raw)
})
ircClient.AddCallback("482", func(e *irc.Event) {
log.Errorf("482 ERR_CHANOPRIVSNEEDED, %s", e.Raw)
})
@@ -286,6 +412,10 @@ func ircClientRun() {
log.Errorf("485 ERR_UNIQOPPRIVSNEEDED, %s", e.Raw)
})
ircClient.AddCallback("491", func(e *irc.Event) {
log.Errorf("491 ERR_NOOPERHOST, %s", e.Raw)
})
ircClient.AddCallback("501", func(e *irc.Event) {
log.Errorf("501 ERR_UMODEUNKNOWNFLAG, %s", e.Raw)
})
@@ -294,22 +424,6 @@ func ircClientRun() {
log.Errorf("502 ERR_USERSDONTMATCH, %s", e.Raw)
})
// Сделаем уже что-то полезное! Motd нам уже прислали и теперь можно авторизоваться и джойниться
ircClient.AddCallback("001", func(e *irc.Event) {
if !config.Irc.Sasl && config.Irc.Password != "" && !nickIsUsed {
log.Info("Identifying via NickServ")
message := fmt.Sprintf("identify %s %s", config.Irc.Nick, config.Irc.Password)
imChan <- iMsg{ChatId: "NickServ", Text: message}
}
log.Debug("Trying to join to preconfigured channels")
for _, channel := range config.Irc.Channels {
log.Infof("Joining to %s channel", channel)
ircClient.Join(channel)
}
})
// Навесим коллбэков на другие, интересные нам события
ircClient.AddCallback("KICK", func(e *irc.Event) {
dstNick := e.Arguments[1]
@@ -318,10 +432,11 @@ func ircClientRun() {
if e.Arguments[1] == ircClient.GetNick() {
// Нас кикнули с канала, и мы теряем информацию о MODE-ах пользователей
userMode.Delete(channel)
UserMode.Delete(channel)
// TODO: reason?
log.Warnf("%s kicks us from %s", srcFullNick, channel)
time.Sleep(10 * time.Second)
<-time.NewTimer(10 * time.Second).C
ircClient.Join(channel)
} else {
// Кого-то другого кикнули с канала
@@ -334,21 +449,22 @@ func ircClientRun() {
srcNick := e.Nick
dstNick := e.Arguments[0]
if e.Nick == config.Irc.Nick {
if e.Nick == Config.Irc.Nick {
nickIsUsed = false
if config.Irc.Password != "" {
if Config.Irc.Password != "" {
log.Warn("I regain my nick")
} else {
log.Warn("I regain my nick, trying to identify myself via NickServ")
message := fmt.Sprintf("identify %s %s", config.Irc.Nick, config.Irc.Password)
imChan <- iMsg{ChatId: "NickServ", Text: message}
message := fmt.Sprintf("identify %s %s", Config.Irc.Nick, Config.Irc.Password)
imChan <- iMsg{ChatID: "NickServ", Text: message}
}
} else {
log.Infof("%s renames themself to %s", srcNick, dstNick)
}
// Неважно чей ник сменился, надо забыть, что было и снова узнать mode-ы сменишего nick джентельмена
// Неважно чей ник сменился, надо забыть, что было и снова узнать mode-ы сменишего nick джентельмена.
// TODO: реализовать userModeRenameUser()
userModePurgeUser(srcNick)
ircClient.Whois(dstNick)
@@ -360,12 +476,12 @@ func ircClientRun() {
channel := e.Arguments[0]
if nick == ircClient.GetNick() {
// Команда names отправляется автоматом.
log.Infof("I joined to %s", channel)
// Команда names отправляется автоматом
} else {
log.Infof("%s joined to %s", fullNick, channel)
// Технически, тут не надо спрашивать whois на пользователя, но мы спрашиваем, чтобы уточнить mode,
// вдруг сервер проставляет mode заранее (хотя не должен)
// вдруг сервер проставляет mode заранее (хотя не должен).
ircClient.Whois(nick)
}
})
@@ -377,7 +493,7 @@ func ircClientRun() {
if nick == ircClient.GetNick() {
log.Infof("I parted from %s", channel)
userMode.Delete(channel)
UserMode.Delete(channel)
} else {
log.Infof("%s parted from %s", fullNick, channel)
userModeDeleteUser(channel, nick)
@@ -403,6 +519,7 @@ func ircClientRun() {
channel := e.Arguments[0]
fullSrcNick := e.Source
srcNick := e.Nick
var dstNick string
if len(e.Arguments) >= 3 { // кого-то по-MODE-или на канале
@@ -415,6 +532,7 @@ func ircClientRun() {
}
} else { // Установка mode-а при заходе на сервер
dstNick = e.Arguments[0]
log.Infof("Server set my mode to %s", mode)
}
@@ -454,17 +572,16 @@ func ircClientRun() {
ircMsgParser(e.Arguments[0], e.Nick, e.User, e.Source, e.Arguments[1])
})
/*
ircClient.AddCallback("*", func(e *irc.Event) {
log.Debugf("Incoming EVENT (Raw): %s", e.Raw)
})
*/
ircClient.AddCallback("*", func(e *irc.Event) {
log.Debugf("Incoming EVENT (Raw): %s", e.Raw)
})
log.Debugf("Make an actual connection to irc")
if err := ircClient.Connect(serverString); err != nil {
log.Fatalf("Unable to prepare irc connection: %s", err)
time.Sleep(3 * time.Second)
log.Errorf("Unable to prepare irc connection: %s", err)
<-time.NewTimer(3 * time.Second).C // Sleep for 3 seconds.
continue
}
@@ -474,26 +591,27 @@ func ircClientRun() {
// ircSend отправляет сообщение в irc. Якобы с применением ratelimit-ов, но на практике длинные сообщения разделяются
// на более короткие и тут ratelimit не срабатывает. Если не повезёт, то сервер может за такое дело и кикнуть.
func ircSend() {
func IrcSend() {
for {
m := <-imChan
switch {
case config.Irc.RateLimit.Type == "simple_delay":
log.Debugf("Sending to chat %s message: %s", m.ChatId, m.Text)
switch Config.Irc.RateLimit.Type {
case "simple_delay":
log.Debugf("Sending to chat %s message: %s", m.ChatID, m.Text)
if m.Text[0:4] == "/me " {
ircClient.Action(m.ChatId, m.Text)
ircClient.Action(m.ChatID, m.Text)
} else {
ircClient.Privmsg(m.ChatId, m.Text)
ircClient.Privmsg(m.ChatID, m.Text)
}
sleepDelay := time.Duration(config.Irc.RateLimit.SimpleDelay) * time.Millisecond
sleepDelay := time.Duration(Config.Irc.RateLimit.SimpleDelay) * time.Millisecond
log.Debugf("Due to delay type simple_delay waiting for %d milliseconds", int(sleepDelay))
time.Sleep(sleepDelay)
case config.Irc.RateLimit.Type == "token_bucket":
<-time.NewTimer(sleepDelay).C
case "token_bucket":
var currentTimeMs = time.Now().UnixMilli()
var expirationTimeMs = config.Irc.RateLimit.TokenBucket.ExpirationTime * 1000
var expirationTimeMs = Config.Irc.RateLimit.TokenBucket.ExpirationTime * 1000
if len(msgBucket.Timestamps) > 0 {
var newBucket bucket
@@ -505,10 +623,11 @@ func ircSend() {
}
bucketFill := len(newBucket.Timestamps)
log.Debugf("Message bucket filled with %d/%d messages", bucketFill, config.Irc.RateLimit.TokenBucket.Size)
log.Debugf("Message bucket filled with %d/%d messages", bucketFill, Config.Irc.RateLimit.TokenBucket.Size)
newBucket.Timestamps = append(newBucket.Timestamps, currentTimeMs)
if bucketFill >= config.Irc.RateLimit.TokenBucket.Size {
if bucketFill >= Config.Irc.RateLimit.TokenBucket.Size {
newBucket.IsFull = true
}
@@ -516,52 +635,55 @@ func ircSend() {
} else {
// Проставим время отправки сообщения
log.Debug("Message bucket is empty")
msgBucket.Timestamps = append(msgBucket.Timestamps, currentTimeMs)
}
if msgBucket.IsFull {
log.Debug("Message bucket overflowed, hitting ratelimit")
sleepPeriod := expirationTimeMs - (currentTimeMs - msgBucket.Timestamps[len(msgBucket.Timestamps)-1])
if config.Irc.RateLimit.TokenBucket.Limit > 0 {
sleepPeriod = sleepPeriod / int64(config.Irc.RateLimit.TokenBucket.Limit)
if Config.Irc.RateLimit.TokenBucket.Limit > 0 {
sleepPeriod /= int64(Config.Irc.RateLimit.TokenBucket.Limit)
}
log.Debugf("Sleeping for %d milliseconds", sleepPeriod)
time.Sleep(time.Duration(sleepPeriod) * time.Millisecond)
<-time.NewTimer(time.Duration(sleepPeriod) * time.Millisecond).C
currentTimeMs = time.Now().UnixMilli()
// Обновим время отправки сообщения
msgBucket.Timestamps[len(msgBucket.Timestamps)-1] = currentTimeMs
}
log.Debugf("Sending to chat %s message: %s", m.ChatId, m.Text)
log.Debugf("Sending to chat %s message: %s", m.ChatID, m.Text)
if len(m.Text) > 4 && m.Text[0:4] == "/me " {
ircClient.Action(m.ChatId, m.Text[4:])
ircClient.Action(m.ChatID, m.Text[4:])
} else {
ircClient.Privmsg(m.ChatId, m.Text)
ircClient.Privmsg(m.ChatID, m.Text)
}
default:
log.Debugf("Sending to chat %s message: %s", m.ChatId, m.Text)
log.Debugf("Sending to chat %s message: %s", m.ChatID, m.Text)
if len(m.Text) > 4 && m.Text[0:4] == "/me " {
ircClient.Action(m.ChatId, m.Text[4:])
ircClient.Action(m.ChatID, m.Text[4:])
} else {
ircClient.Privmsg(m.ChatId, m.Text)
ircClient.Privmsg(m.ChatID, m.Text)
}
}
}
}
func ircSendUnrestricted() {
func IrcSendUnrestricted() {
for {
m := <-imChanUnrestricted
log.Debugf("Skipping ratelimit and sending to chat %s message: %s", m.ChatId, m.Text)
log.Debugf("Skipping ratelimit and sending to chat %s message: %s", m.ChatID, m.Text)
if len(m.Text) > 4 && m.Text[0:4] == "/me " {
ircClient.Action(m.ChatId, m.Text[4:])
ircClient.Action(m.ChatID, m.Text[4:])
} else {
ircClient.Privmsg(m.ChatId, m.Text)
ircClient.Privmsg(m.ChatID, m.Text)
}
}
}

View File

@@ -1,50 +1,55 @@
package main
package irc
import (
"context"
"os"
"github.com/cockroachdb/pebble"
"aleesa-irc-go/internal/anycollection"
"aleesa-irc-go/internal/boolcollection"
"github.com/go-redis/redis/v8"
irc "github.com/thoj/go-ircevent"
)
// Config - это у нас глобальная штука :)
var config myConfig
// Config - это у нас глобальная штука :).
var Config MyConfig
// To break circular message forwarding we must set some sane default, it can be overridden via config
// To break circular message forwarding we must set some sane default, it can be overridden via config.
var forwardMax int64 = 5
// Объектики irc-клиента
// Объектики irc-клиента.
var ircClient *irc.Connection
var nickIsUsed = false
// Объектики клиента-редиски
var redisClient *redis.Client
var subscriber *redis.PubSub
// Объектики клиента-редиски.
var RedisClient *redis.Client
var Subscriber *redis.PubSub
// Main context
// Main context.
var ctx = context.Background()
// Ставится в true, если мы получили сигнал на выключение
var shutdown = false
// Ставится в true, если мы получили сигнал на выключение.
var Shutdown = false
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов
var sigChan = make(chan os.Signal, 1)
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
var SigChan = make(chan os.Signal, 1)
// Канал, в который пишутся сообщения для отправки в IRC в обычном порядке
// Канал, в который пишутся сообщения для отправки в IRC в обычном порядке.
var imChan = make(chan iMsg, 10000)
// Канал, в который пишутся сообщения для отправки в IRC, если ограничений нет (у бота +o или +v на канале)
// Канал, в который пишутся сообщения для отправки в IRC, если ограничений нет (у бота +o или +v на канале).
var imChanUnrestricted = make(chan iMsg, 100)
// "Ведёрко" с timestamp-ами последних отправленных сообщений
// "Ведёрко" с timestamp-ами последних отправленных сообщений.
var msgBucket bucket
// Мапка с открытыми дескрипторами баз с настройками
var settingsDB = make(map[string]*pebble.DB)
// "Базюлька" с MODE-ами пользователей на каналах.
var UserMode = anycollection.NewCollection()
// "Базюлька" с MODE-ами пользователей на каналах
var userMode = NewCollection()
// "Базюлька" с доступными MODE-ами пользователя.
var availableUserModes = boolcollection.NewCollection()
// "Базюлька" c с доступными MODE-ами каналов.
var availableChanModes = boolcollection.NewCollection()
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,4 +1,4 @@
package main
package irc
import "strings"
@@ -8,33 +8,34 @@ import "strings"
активируют мягкий ratelimit на стороне сервера, без них, сервер просто рвёт соединение по превышению лимитов
*/
// Удаляет сведения о MODE-ах пользователя для заданного канала
// Удаляет сведения о MODE-ах пользователя для заданного канала.
func userModeDeleteUser(ircChan string, nick string) {
channel, ok := userMode.Get(ircChan)
channel, ok := UserMode.Get(ircChan)
if ok {
channelData := channel.(map[string]map[string]bool)
delete(channelData, nick)
if len(channelData) == 0 {
userMode.Delete(ircChan)
UserMode.Delete(ircChan)
} else {
userMode.Set(ircChan, channelData)
UserMode.Set(ircChan, channelData)
}
}
}
// Удаляет сведения о MODE-ах пользователя для всех каналов, на которых есть бот
// Удаляет сведения о MODE-ах пользователя для всех каналов, на которых есть бот.
func userModePurgeUser(nick string) {
for _, channel := range config.Irc.Channels {
for _, channel := range Config.Irc.Channels {
userModeDeleteUser(channel, nick)
}
}
// Обновляет или создаёт запись о mode-ах пользователей
// Обновляет или создаёт запись о mode-ах пользователей.
func userModeUpdateUser(ircChan string, nick string, modes string) {
var channelData map[string]map[string]bool
channel, ok := userMode.Get(ircChan)
channel, ok := UserMode.Get(ircChan)
if ok {
channelData = channel.(map[string]map[string]bool)
@@ -48,24 +49,25 @@ func userModeUpdateUser(ircChan string, nick string, modes string) {
channelData[nick] = make(map[string]bool)
}
switch {
case modes[:1] == "+":
switch modes[:1] {
case "+":
for _, mode := range strings.Split(modes[1:], "") {
channelData[nick][mode] = true
}
case modes[:1] == "-":
case "-":
for _, mode := range strings.Split(modes[1:], "") {
channelData[nick][mode] = false
}
}
userMode.Set(ircChan, channelData)
UserMode.Set(ircChan, channelData)
}
// Возвращает значение MODE-а v для запрошенного ника
// Возвращает значение MODE-а v для запрошенного ника.
func userModeIsVoiced(ircChan string, nick string) bool {
var channelData map[string]map[string]bool
channel, ok := userMode.Get(ircChan)
channel, ok := UserMode.Get(ircChan)
if ok {
channelData = channel.(map[string]map[string]bool)
@@ -88,10 +90,11 @@ func userModeIsVoiced(ircChan string, nick string) bool {
return false
}
// Возвращает значение MODE-а o для запрошенного ника
// Возвращает значение MODE-а o для запрошенного ника.
func userModeIsOped(ircChan string, nick string) bool {
var channelData map[string]map[string]bool
channel, ok := userMode.Get(ircChan)
channel, ok := UserMode.Get(ircChan)
if ok {
channelData = channel.(map[string]map[string]bool)
@@ -116,7 +119,8 @@ func userModeIsOped(ircChan string, nick string) bool {
func userModeIsHere(ircChan string, nick string) bool {
var channelData map[string]map[string]bool
channel, ok := userMode.Get(ircChan)
channel, ok := UserMode.Get(ircChan)
if ok {
channelData = channel.(map[string]map[string]bool)

View File

@@ -1,4 +1,4 @@
package main
package irc
import (
"encoding/json"
@@ -6,15 +6,16 @@ import (
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"aleesa-irc-go/internal/log"
"aleesa-irc-go/internal/setting"
)
// ircMsgParser парсит сообщения, прилетевшие из IRC-ки
func ircMsgParser(channel string, nick string, user string, source string, msg string) {
// ircMsgParser парсит сообщения, прилетевшие из IRC-ки.
func ircMsgParser(channel string, nick string, user string, source string, msg string) { //nolint: revive
// nick - это выбранный пользователем nick (если он занят, то его "нарисует" сервер)
// user - это короткое имя пользователя, под которым его видит сервер
// source - это длинное имя пользователя, оно содержит в себе помимо user, ещё и ip с которого пришёл пользователь
if shutdown {
if Shutdown {
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо
return
}
@@ -26,89 +27,90 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
}
// Ловим команды и обрабатываем их
if (len(msg) > len(config.Csign)) && (msg[:len(config.Csign)] == config.Csign) {
if (len(msg) > len(Config.Csign)) && (msg[:len(Config.Csign)] == Config.Csign) {
var outgoingMessage string
var message sMsg
message.From = config.Redis.MyChannel
message.From = Config.Redis.MyChannel
message.Userid = user // как его видит сервер
message.Chatid = channel // чятик, в который написал user
message.Plugin = config.Redis.MyChannel
message.Threadid = "" // тредиков в irc нету, поэтому это поле отправляем пустым
message.Plugin = Config.Redis.MyChannel
message.Mode = "public"
// Предполагается, что на команды бот автоматом отвечает
message.Misc.Answer = 1
message.Misc.Fwdcnt = 0
message.Misc.Csign = config.Csign
message.Misc.Csign = Config.Csign
message.Misc.Username = nick
message.Misc.Botnick = config.Irc.Nick
message.Misc.Botnick = Config.Irc.Nick
message.Misc.Msgformat = 0
var cmd = msg[len(config.Csign):]
var cmd = msg[len(Config.Csign):]
switch {
case cmd == "help" || msg == "помощь":
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%shelp | %sпомощь - это сообщение", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", config.Csign, config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sbuni - комикс-стрип hapi buni", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sbunny - кролик", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%srabbit | %sкролик - кролик", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%scat | %sкис - кошечка", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sdice | %sroll | %sкости - бросить кости", config.Csign, config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sdig | %sкопать - заняться археологией", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sdrink | %sпраздник - какой сегодня праздник?", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sfish | %sfisher - порыбачить", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sрыба | %sрыбка | %sрыбалка - порыбачить", config.Csign, config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sf | %sф - рандомная фраза из сборника цитат fortune_mod", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sfortune | %sфортунка - рандомная фраза из сборника цитат fortune_mod", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sfox | %sлис - лисичка", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sfriday | %sпятница - а не пятница ли сегодня?", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sfrog | %sлягушка - лягушка", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%shorse | %sлошадь | %sлошадка - лошадка", config.Csign, config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%skarma фраза - посмотреть карму фразы", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sкарма фраза - посмотреть карму фразы", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintln("фраза++ | фраза-- - повысить или понизить карму фразы")}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%slat | %sлат - сгенерировать фразу из крылатого латинского выражения", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%smonkeyuser - комикс-стрип MonkeyUser", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sowl | %sсова - сова", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sping | %sпинг - попинговать бота", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sproverb | %sпословица - рандомная русская пословица", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%ssnail | %sулитка - улитка", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%ssome_brew - выдать соответствующий напиток, бармен может налить rum, ром, vodka, водку, tequila, текила, whisky, виски, absinthe, абсент", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sver | %sversion - написать что-то про версию ПО", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sверсия - написать что-то про версию ПО", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sw город | %sп город - погода в городе", config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sxkcd - комикс-стрип с xkcb.ru", config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%shelp | %sпомощь - это сообщение", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", Config.Csign, Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sbuni - комикс-стрип hapi buni", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sbunny - кролик", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%srabbit | %sкролик - кролик", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%scat | %sкис - кошечка", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sdice | %sroll | %sкости - бросить кости", Config.Csign, Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sdig | %sкопать - заняться археологией", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sdrink | %sпраздник - какой сегодня праздник?", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sfish | %sfisher - порыбачить", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sрыба | %sрыбка | %sрыбалка - порыбачить", Config.Csign, Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sf | %sф - рандомная фраза из сборника цитат fortune_mod", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sfortune | %sфортунка - рандомная фраза из сборника цитат fortune_mod", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sfox | %sлис - лисичка", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sfriday | %sпятница - а не пятница ли сегодня?", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sfrog | %sлягушка - лягушка", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%shorse | %sлошадь | %sлошадка - лошадка", Config.Csign, Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%skarma фраза - посмотреть карму фразы", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sкарма фраза - посмотреть карму фразы", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintln("фраза++ | фраза-- - повысить или понизить карму фразы")}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%slat | %sлат - сгенерировать фразу из крылатого латинского выражения", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%smonkeyuser - комикс-стрип MonkeyUser", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sowl | %sсова - сова", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sping | %sпинг - попинговать бота", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sproverb | %sпословица - рандомная русская пословица", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%ssnail | %sулитка - улитка", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%ssome_brew - выдать соответствующий напиток, бармен может налить rum, ром, vodka, водку, tequila, текила, whisky, виски, absinthe, абсент", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sver | %sversion - написать что-то про версию ПО", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sверсия - написать что-то про версию ПО", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sw <город> | %sп <город> - погода в городе", Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sxkcd - комикс-стрип с xkcb.ru", Config.Csign)}
if userModeIsOped(channel, nick) {
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sadmin - настройки некоторых плагинов бота для канала", config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sadmin - настройки некоторых плагинов бота для канала", Config.Csign)}
}
return
case cmd == "admin":
if userModeIsOped(channel, nick) {
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sadmin oboobs # - где 1 - вкл, 0 - выкл плагина oboobs", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sadmin oboobs показываем ли сисечки по просьбе участников чата (команды %stits, %stities, %sboobs, %sboobies, %sсиси, %sсисечки)", config.Csign, config.Csign, config.Csign, config.Csign, config.Csign, config.Csign, config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sadmin obutts # - где 1 - вкл, 0 - выкл плагина obutts", config.Csign)}
imChan <- iMsg{ChatId: nick, Text: fmt.Sprintf("%sadmin obutts показываем ли попки по просьбе участников чата (команды %sass, %sbutt, %sbooty, %sпопа, %sпопка)", config.Csign, config.Csign, config.Csign, config.Csign, config.Csign, config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sadmin oboobs # - где 1 - вкл, 0 - выкл плагина oboobs", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sadmin oboobs показываем ли сисечки по просьбе участников чата (команды %stits, %stities, %sboobs, %sboobies, %sсиси, %sсисечки)", Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sadmin obutts # - где 1 - вкл, 0 - выкл плагина obutts", Config.Csign)}
imChan <- iMsg{ChatID: nick, Text: fmt.Sprintf("%sadmin obutts показываем ли попки по просьбе участников чата (команды %sass, %sbutt, %sbooty, %sпопа, %sпопка)", Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign)}
}
return
case cmd == "admin oboobs":
if userModeIsOped(channel, nick) {
value := getSetting(channel, "oboobs")
value := setting.Get(channel, "oboobs")
switch {
case value == "":
_ = saveSetting(channel, "oboobs", "0")
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs выключен"}
case value == "0":
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs выключен"}
case value == "1":
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs включен"}
switch value {
case "":
_ = setting.Set(channel, "oboobs", "0")
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs выключен"}
case "0":
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs выключен"}
case "1":
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs включен"}
default:
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs выключен"}
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs выключен"}
}
}
@@ -116,12 +118,12 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
case cmd == "admin oboobs 1":
if userModeIsOped(channel, nick) {
err := saveSetting(channel, "oboobs", "1")
err := setting.Set(channel, "oboobs", "1")
if err != nil {
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs всё ещё выключен"}
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs всё ещё выключен"}
} else {
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs включен"}
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs включен"}
}
}
@@ -129,26 +131,26 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
case cmd == "admin oboobs 0":
if userModeIsOped(channel, nick) {
_ = saveSetting(channel, "oboobs", "0")
imChan <- iMsg{ChatId: nick, Text: "Плагин oboobs выключен"}
_ = setting.Set(channel, "oboobs", "0")
imChan <- iMsg{ChatID: nick, Text: "Плагин oboobs выключен"}
}
return
case cmd == "admin obutts":
if userModeIsOped(channel, nick) {
value := getSetting(channel, "obutts")
value := setting.Get(channel, "obutts")
switch {
case value == "":
_ = saveSetting(channel, "obutts", "0")
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts выключен"}
case value == "0":
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts выключен"}
case value == "1":
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts включен"}
switch value {
case "":
_ = setting.Set(channel, "obutts", "0")
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts выключен"}
case "0":
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts выключен"}
case "1":
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts включен"}
default:
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts выключен"}
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts выключен"}
}
}
@@ -156,12 +158,12 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
case cmd == "admin obutts 1":
if userModeIsOped(channel, nick) {
err := saveSetting(channel, "obutts", "1")
err := setting.Set(channel, "obutts", "1")
if err != nil {
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts всё ещё выключен"}
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts всё ещё выключен"}
} else {
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts включен"}
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts включен"}
}
}
@@ -169,8 +171,8 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
case cmd == "admin obutts 0":
if userModeIsOped(channel, nick) {
_ = saveSetting(channel, "obutts", "0")
imChan <- iMsg{ChatId: nick, Text: "Плагин obutts выключен"}
_ = setting.Set(channel, "obutts", "0")
imChan <- iMsg{ChatID: nick, Text: "Плагин obutts выключен"}
}
return
@@ -181,15 +183,17 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
// Общий список простых команд
cmds := []string{"ping", "пинг", "пинх", "pong", "понг", "понх", "coin", "монетка", "roll", "dice", "кости",
"ver", "version", "версия", "хэлп", "halp", "kde", "кде", "lat", "лат", "friday", "пятница", "proverb",
"пословица", "fortune", "фортунка", "f", "ф", "anek", "анек", "анекдот", "buni", "cat", "кис", "drink",
"праздник", "fox", "лис", "frog", "лягушка", "horse", "лошадь", "лошадка", "monkeyuser", "owl", "сова",
"сыч", "rabbit", "bunny", "кролик", "snail", "улитка", "xkcd", "dig", "копать", "fish", "fishing", "рыба",
"рыбка", "рыбалка", "karma", "карма"}
"пословица", "пословиться", "fortune", "фортунка", "f", "ф", "anek", "анек", "анекдот", "buni", "cat",
"кис", "drink", "праздник", "fox", "лис", "frog", "лягушка", "horse", "лошадь", "лошадка", "monkeyuser",
"owl", "сова", "сыч", "rabbit", "bunny", "кролик", "snail", "улитка", "xkcd", "dig", "копать", "fish",
"fishing", "рыба", "рыбка", "рыбалка", "karma", "карма", "w", "п", "weather", "погода", "погодка",
"погадка"}
for _, command := range cmds {
if cmd == command {
done = true
outgoingMessage = msg
break
}
}
@@ -204,6 +208,7 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
if cmd == command {
done = true
outgoingMessage = msg
break
}
}
@@ -224,7 +229,8 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
message.Misc.Username = strings.TrimSpace(pile[1])
} else {
msg = fmt.Sprintf("Я тут не вижу участника с ником %s", userNick)
imChan <- iMsg{ChatId: channel, Text: msg}
imChan <- iMsg{ChatID: channel, Text: msg}
return
}
} // в противном случае юзер просто поставил пробел на конце команды, интерпретируем это как
@@ -232,6 +238,7 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
outgoingMessage = msg
done = true
break
}
}
@@ -248,6 +255,7 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
if cmdLen > len(command) && cmd[0:len(command)] == command {
done = true
outgoingMessage = msg
break
}
}
@@ -255,7 +263,7 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
// Отключаемые команды
if !done {
value := getSetting(channel, "obutts")
value := setting.Get(channel, "obutts")
if value == "1" {
cmds := []string{"butt", "booty", "ass", "попа", "попка"}
@@ -265,13 +273,14 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
if cmd[:cmdLen] == command {
done = true
outgoingMessage = msg
break
}
}
}
if !done {
value = getSetting(channel, "oboobs")
value = setting.Get(channel, "oboobs")
if value == "1" {
cmds := []string{"tits", "boobs", "tities", "boobies", "сиси", "сисечки"}
@@ -281,6 +290,7 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
if cmd[:cmdLen] == command {
// done = true
outgoingMessage = msg
break
}
}
@@ -295,24 +305,26 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
if err != nil {
log.Warnf("Unable to to serialize message for redis: %s", err)
return
}
// Заталкиваем наш json в редиску
if err := redisClient.Publish(ctx, config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", config.Redis.Channel, err)
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else {
log.Debugf("Sent msg to redis channel %s: %s", config.Redis.Channel, string(data))
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
}
}
} else {
// Это уже просто трёп в чятике
var message sMsg
message.From = config.Redis.MyChannel
message.From = Config.Redis.MyChannel
message.Userid = user // как его видит сервер
message.Chatid = channel // чятик, в который написал user
message.Threadid = "" // тредиков в irc нету, поэтому это поле отправляем пустым
message.Message = msg
message.Plugin = config.Redis.MyChannel
message.Plugin = Config.Redis.MyChannel
message.Mode = "public"
// Если во фразе содержится ключевые слова, под это есть костыль в craniac-е - (предполагаем, что) он
@@ -339,32 +351,31 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s
}
message.Misc.Fwdcnt = 0
message.Misc.Csign = config.Csign
message.Misc.Csign = Config.Csign
message.Misc.Username = nick
message.Misc.Botnick = config.Irc.Nick
message.Misc.Botnick = Config.Irc.Nick
message.Misc.Msgformat = 0
data, err := json.Marshal(message)
if err != nil {
log.Warnf("Unable to to serialize message for redis: %s", err)
return
}
// Заталкиваем наш json в редиску
if err := redisClient.Publish(ctx, config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", config.Redis.Channel, err)
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else {
log.Debugf("Sent msg to redis channel %s: %s", config.Redis.Channel, string(data))
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
}
}
return
}
// redisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём, json-чики должны быть относительно валидными
func redisMsgParser(msg string) {
if shutdown {
// RedisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём, json-чики должны быть относительно валидными.
func RedisMsgParser(msg string) {
if Shutdown {
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо
return
}
@@ -375,50 +386,58 @@ func redisMsgParser(msg string) {
if err := json.Unmarshal([]byte(msg), &j); err != nil {
log.Warnf("Unable to to parse message from redis channel: %s", err)
return
}
// Validate our j
if exist := j.From; exist == "" {
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
return
}
if exist := j.Chatid; exist == "" {
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
return
}
if exist := j.Userid; exist == "" {
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
return
}
if exist := j.Message; exist == "" {
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
return
}
if exist := j.Plugin; exist == "" {
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
return
}
if exist := j.Mode; exist == "" {
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
return
}
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
if j.Misc.Answer == 0 {
log.Debug("Field Misc->Answer = 0, skipping message")
return
}
// j.Misc.BotNick тоже можно не передавать, тогда будет записана пустая строка
// j.Misc.CSign если нам его не передали, возьмём значение из конфига
if exist := j.Misc.Csign; exist == "" {
j.Misc.Csign = config.Csign
j.Misc.Csign = Config.Csign
}
// j.Misc.FwdCnt если нам его не передали, то будет 0
@@ -435,13 +454,11 @@ func redisMsgParser(msg string) {
for _, message := range lines {
if userModeIsOped(j.Chatid, ircClient.GetNick()) || userModeIsVoiced(j.Chatid, ircClient.GetNick()) {
imChanUnrestricted <- iMsg{ChatId: j.Chatid, Text: message}
imChanUnrestricted <- iMsg{ChatID: j.Chatid, Text: message}
} else {
imChan <- iMsg{ChatId: j.Chatid, Text: message}
imChan <- iMsg{ChatID: j.Chatid, Text: message}
}
}
return
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,7 +1,7 @@
package main
package irc
// Конфиг
type myConfig struct {
// Конфиг.
type MyConfig struct {
Redis struct {
Server string `json:"server,omitempty"`
Port int `json:"port,omitempty"`
@@ -35,15 +35,16 @@ type myConfig struct {
DataDir string `json:"data_dir,omitempty"`
}
// Входящее сообщение из pubsub-канала redis-ки
// Входящее сообщение из pubsub-канала redis-ки.
type rMsg struct {
From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"`
Message string `json:"message,omitempty"`
Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"`
Misc struct {
From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"`
Threadid string `json:"threadid,omitempty"`
Message string `json:"message,omitempty"`
Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"`
Misc struct {
Answer int64 `json:"answer,omitempty"`
Botnick string `json:"bot_nick,omitempty"`
Csign string `json:"csign,omitempty"`
@@ -54,15 +55,16 @@ type rMsg struct {
} `json:"Misc"`
}
// Исходящее сообщение в pubsub-канал redis-ки
// Исходящее сообщение в pubsub-канал redis-ки.
type sMsg struct {
From string `json:"from"`
Chatid string `json:"chatid"`
Userid string `json:"userid"`
Message string `json:"message"`
Plugin string `json:"plugin"`
Mode string `json:"mode"`
Misc struct {
From string `json:"from"`
Chatid string `json:"chatid"`
Userid string `json:"userid"`
Threadid string `json:"threadid"`
Message string `json:"message"`
Plugin string `json:"plugin"`
Mode string `json:"mode"`
Misc struct {
Answer int64 `json:"answer"`
Botnick string `json:"bot_nick"`
Csign string `json:"csign"`
@@ -73,14 +75,13 @@ type sMsg struct {
} `json:"misc"`
}
// Сообщение в irc
// TODO: Имплементировать тип сообщения - privmsg или action
// TODO: Имплементировать тип сообщения - privmsg или action.
type iMsg struct {
ChatId string
ChatID string
Text string
}
// "Ведёрко" для реализации ограничителя скорости отправки исходящих сообщений - Token Bucket
// "Ведёрко" для реализации ограничителя скорости отправки исходящих сообщений - Token Bucket.
type bucket struct {
Timestamps []int64
IsFull bool

View File

@@ -1,24 +1,25 @@
package main
package irc
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"aleesa-irc-go/internal/log"
"aleesa-irc-go/internal/setting"
"github.com/hjson/hjson-go"
log "github.com/sirupsen/logrus"
)
// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет
func readConfig() {
// ReadConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет.
func ReadConfig() {
configLoaded := false
executablePath, err := os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
log.Error(fmt.Sprintf("Unable to get current executable path: %s", err))
}
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
@@ -41,27 +42,31 @@ func readConfig() {
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
if fileInfo.Size() > 65535 {
log.Warnf("Config file %s is too long for config, skipping", location)
continue
}
buf, err := ioutil.ReadFile(location)
buf, err := os.ReadFile(location)
// Не удалось прочитать, попробуем следующего кандидата
if err != nil {
log.Warnf("Skip reading config file %s: %s", location, err)
continue
}
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
var sampleConfig myConfig
var sampleConfig MyConfig
var tmp map[string]interface{}
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
if err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
continue
}
@@ -70,17 +75,20 @@ func readConfig() {
// Не удалось преобразовать map-ку в json
if err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
continue
}
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
continue
}
// Валидируем значения из конфига
if sampleConfig.Redis.Server == "" {
sampleConfig.Redis.Server = "localhost"
log.Infof("Redis server is not defined in config, using localhost")
}
@@ -101,15 +109,17 @@ func readConfig() {
// Значения для IRC-клиента
if sampleConfig.Irc.Server == "" {
sampleConfig.Irc.Server = "localhost"
log.Errorf("Irc server is not defined in config, using localhost")
}
if sampleConfig.Irc.Port == 0 {
sampleConfig.Irc.Port = 6667
log.Infof("Irc port is not defined in config, using 6667")
}
if sampleConfig.Irc.Ssl != true {
if !sampleConfig.Irc.Ssl {
sampleConfig.Irc.SslVerify = false
}
@@ -180,9 +190,11 @@ func readConfig() {
os.Exit(1)
}
config = sampleConfig
Config = sampleConfig
configLoaded = true
log.Infof("Using %s as config file", location)
break
}
@@ -192,19 +204,19 @@ func readConfig() {
}
}
// Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения
func sigHandler() {
// SigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
func SigHandler() {
var err error
for {
var s = <-sigChan
var s = <-SigChan
switch s {
case syscall.SIGINT:
log.Infoln("Got SIGINT, quitting")
log.Info("Got SIGINT, quitting")
case syscall.SIGTERM:
log.Infoln("Got SIGTERM, quitting")
log.Info("Got SIGTERM, quitting")
case syscall.SIGQUIT:
log.Infoln("Got SIGQUIT, quitting")
log.Info("Got SIGQUIT, quitting")
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
default:
@@ -212,16 +224,16 @@ func sigHandler() {
}
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true
shutdown = true
Shutdown = true
// Отпишемся от всех каналов и закроем коннект к редиске
if err = subscriber.Unsubscribe(ctx); err != nil {
if err = Subscriber.Unsubscribe(ctx); err != nil {
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
} else {
log.Debug("Unsubscribe from all redis channels")
}
if err = subscriber.Close(); err != nil {
if err = Subscriber.Close(); err != nil {
log.Errorf("Unable to close redis connection cleanly: %s", err)
} else {
log.Debug("Close redis connection")
@@ -229,16 +241,12 @@ func sigHandler() {
log.Debug("Close irc connection")
ircClient.Quit()
log.Debug("Close userModeDB")
userMode.Close()
UserMode.Close()
if len(settingsDB) > 0 {
log.Debug("Closing runtime irc channel settings db")
for _, db := range settingsDB {
_ = db.Close()
}
}
log.Debug("Close settingsDB")
setting.CloseAll()
os.Exit(0)
}

106
internal/log/log.go Normal file
View File

@@ -0,0 +1,106 @@
package log
import (
"fmt"
"log/slog"
"os"
"time"
)
// Init setup logger stuff.
// level can be error, warn, info, debug if something other supplied info level selected.
// fileDescriptor should be opened before supplying it to Init().
func Init(level string, fileDescriptor *os.File) {
var loglevel slog.Level
// no panic, no trace.
switch level {
case "error":
loglevel = slog.LevelError
case "warn":
loglevel = slog.LevelWarn
case "info":
loglevel = slog.LevelInfo
case "debug":
loglevel = slog.LevelDebug
default:
loglevel = slog.LevelInfo
}
opts := &slog.HandlerOptions{
// Use the ReplaceAttr function on the handler options
// to be able to replace any single attribute in the log output
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// check that we are handling the time key
if a.Key != slog.TimeKey {
return a
}
t := a.Value.Time()
// change the value from a time.Time to a String
// where the string has the correct time format.
a.Value = slog.StringValue(t.Format(time.DateTime))
return a
},
Level: loglevel,
}
slog.SetDefault(
slog.New(
slog.NewTextHandler(
fileDescriptor,
opts,
),
),
)
}
// Error passes message directly to slog.Error().
func Error(message string) {
slog.Error(message)
}
// Errorf logs message on error log level and allows to supply arguments in printf() style.
func Errorf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
}
// Warn passes message directly to slog.Warn().
func Warn(message string) {
slog.Warn(message)
}
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
func Warnf(format string, a ...any) {
slog.Warn(fmt.Sprintf(format, a...))
}
// Info passes message directly to slog.Info().
func Info(message string) {
slog.Info(message)
}
// Infof logs message on info log level and allows to supply arguments in printf() style.
func Infof(format string, a ...any) {
slog.Info(fmt.Sprintf(format, a...))
}
// Debug passes message directly to slog.Debug().
func Debug(message string) {
slog.Debug(message)
}
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
func Debugf(format string, a ...any) {
slog.Debug(fmt.Sprintf(format, a...))
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,4 +1,4 @@
package main
package setting
import (
"crypto/sha256"
@@ -6,14 +6,20 @@ import (
"fmt"
"io/fs"
"aleesa-irc-go/internal/log"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble"
log "github.com/sirupsen/logrus"
)
// StoreKV сохраняет в указанной бд ключ и значение
// Мапка с открытыми дескрипторами баз с настройками.
var settingsDB = make(map[string]*pebble.DB)
var DataDir string
// StoreKV сохраняет в указанной бд ключ и значение.
func StoreKV(db *pebble.DB, key string, value string) error {
var kArray = []byte(key)
var vArray = []byte(value)
err := db.Set(kArray, vArray, pebble.Sync)
@@ -21,11 +27,12 @@ func StoreKV(db *pebble.DB, key string, value string) error {
return err
}
// FetchV достаёт значение по ключу
// FetchV достаёт значение по ключу.
func FetchV(db *pebble.DB, key string) (string, error) {
var kArray = []byte(key)
var vArray []byte
var valueString = ""
vArray, closer, err := db.Get(kArray)
@@ -40,8 +47,8 @@ func FetchV(db *pebble.DB, key string) (string, error) {
return valueString, err
}
// Достанем настройку из БД с настройками
func getSetting(chatID string, setting string) string {
// Достанем настройку из БД с настройками.
func Get(chatID string, setting string) string {
var err error
chatHash := sha256.Sum256([]byte(chatID))
@@ -54,10 +61,11 @@ func getSetting(chatID string, setting string) string {
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений
options.L0CompactionFileThreshold = 8
settingsDB[database], err = pebble.Open(config.DataDir+"/"+database, &options)
settingsDB[database], err = pebble.Open(DataDir+"/"+database, &options)
if err != nil {
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
return ""
}
}
@@ -66,13 +74,14 @@ func getSetting(chatID string, setting string) string {
// Если из базы ничего не вынулось, по каким-то причинам, то просто вернём пустую строку
if err != nil {
if errors.Is(err, pebble.ErrNotFound) {
switch {
case errors.Is(err, pebble.ErrNotFound):
log.Debugf("Unable to get value for %s: no record found in db %s", setting, database)
} else if errors.Is(err, fs.ErrNotExist) {
case errors.Is(err, fs.ErrNotExist):
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
} else if errors.Is(err, oserror.ErrNotExist) {
case errors.Is(err, oserror.ErrNotExist):
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
} else {
default:
log.Errorf("Unable to get value for %s in db dir %s: %s", setting, database, err)
}
@@ -82,10 +91,12 @@ func getSetting(chatID string, setting string) string {
return value
}
// Сохраним настройку в БД с настройками
func saveSetting(chatID string, setting string, value string) error {
// Сохраним настройку в БД с настройками.
func Set(chatID string, setting string, value string) error {
var chatHash = sha256.Sum256([]byte(chatID))
var database = fmt.Sprintf("settings_db/%x", chatHash)
var err error
// Если БД не открыта, откроем её
@@ -96,10 +107,11 @@ func saveSetting(chatID string, setting string, value string) error {
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений
options.L0CompactionFileThreshold = 8
settingsDB[database], err = pebble.Open(config.DataDir+"/"+database, &options)
settingsDB[database], err = pebble.Open(DataDir+"/"+database, &options)
if err != nil {
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
return err
}
}
@@ -111,4 +123,15 @@ func saveSetting(chatID string, setting string, value string) error {
return err
}
// Закрываем все инстансы баз с настройками.
func CloseAll() {
if len(settingsDB) > 0 {
log.Debug("Closing runtime irc channel settings db")
for _, db := range settingsDB {
_ = db.Close()
}
}
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

102
main.go
View File

@@ -1,102 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/go-redis/redis/v8"
log "github.com/sirupsen/logrus"
)
// Производит некоторую инициализацию перед запуском main()
func init() {
log.SetFormatter(&log.TextFormatter{
DisableQuote: true,
DisableLevelTruncation: false,
DisableColors: true,
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
readConfig()
// no panic, no trace
switch config.Loglevel {
case "fatal":
log.SetLevel(log.FatalLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "debug":
log.SetLevel(log.DebugLevel)
default:
log.SetLevel(log.InfoLevel)
}
// Проинициализируем мапку MODE-ами пользователей произвольным значением и удалим его
var usersModes = map[string]map[string]map[string]bool{
"user": {
"mode": {
"v": true,
},
},
}
userMode.Set("channel", usersModes)
userMode.Delete("channel")
}
// Собственно, какбэ "точка входа" - основная процедура в нашем боте
func main() {
// Main context
var ctx = context.Background()
// Откроем лог и скормим его логгеру
if config.Log != "" {
logfile, err := os.OpenFile(config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
log.Fatalf("Unable to open log file %s: %s", config.Log, err)
}
log.SetOutput(logfile)
}
// Иницализируем redis-клиента
redisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Redis.Server, config.Redis.Port),
})
log.Debugf("Lazy connect() to redis at %s:%d", config.Redis.Server, config.Redis.Port)
subscriber = redisClient.Subscribe(ctx, config.Redis.MyChannel)
redisMsgChan := subscriber.Channel()
go ircClientRun()
go ircSend()
go ircSendUnrestricted()
// Самое время поставить траппер сигналов
signal.Notify(sigChan,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
go sigHandler()
// Обработчик событий от редиски
for msg := range redisMsgChan {
if shutdown {
continue
}
redisMsgParser(msg.Payload)
}
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,45 +0,0 @@
# Go parameters
GOCMD=GO111MODULE=on go
GOBUILD=$(GOCMD) build
GOINSTALL=$(GOCMD) install
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOFMT=$(GOCMD) fmt
GODOC=godoc
.PHONY: all test coverage
all: test
checkfmt:
@echo 'Checking gofmt';\
bash -c "diff -u <(echo -n) <(gofmt -d .)";\
EXIT_CODE=$$?;\
if [ "$$EXIT_CODE" -ne 0 ]; then \
echo '$@: Go files must be formatted with gofmt'; \
fi && \
exit $$EXIT_CODE
lint:
$(GOGET) github.com/golangci/golangci-lint/cmd/golangci-lint
golangci-lint run
get:
$(GOGET) -v ./...
fmt:
$(GOFMT) ./...
test: get fmt
$(GOTEST) -count=1 ./...
coverage: get test
$(GOTEST) -count=1 -race -coverprofile=coverage.txt -covermode=atomic .
benchmark: get
$(GOTEST) -bench=. -benchmem
godoc:
$(GODOC)

View File

@@ -1,78 +0,0 @@
hdrhistogram-go
===============
<a href="https://pkg.go.dev/github.com/HdrHistogram/hdrhistogram-go"><img src="https://pkg.go.dev/badge/github.com/HdrHistogram/hdrhistogram-go" alt="PkgGoDev"></a>
[![Gitter](https://badges.gitter.im/Join_Chat.svg)](https://gitter.im/HdrHistogram/HdrHistogram)
![Test](https://github.com/HdrHistogram/hdrhistogram-go/workflows/Test/badge.svg?branch=master)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/HdrHistogram/hdrhistogram-go/blob/master/LICENSE)
[![Codecov](https://codecov.io/gh/HdrHistogram/hdrhistogram-go/branch/master/graph/badge.svg)](https://codecov.io/gh/HdrHistogram/hdrhistogram-go)
A pure Go implementation of the [HDR Histogram](https://github.com/HdrHistogram/HdrHistogram).
> A Histogram that supports recording and analyzing sampled data value counts
> across a configurable integer value range with configurable value precision
> within the range. Value precision is expressed as the number of significant
> digits in the value recording, and provides control over value quantization
> behavior across the value range and the subsequent value resolution at any
> given level.
For documentation, check [godoc](https://pkg.go.dev/github.com/HdrHistogram/hdrhistogram-go).
## Getting Started
### Installing
Use `go get` to retrieve the hdrhistogram-go implementation and to add it to your `GOPATH` workspace, or project's Go module dependencies.
```go
go get github.com/HdrHistogram/hdrhistogram-go
```
To update the implementation use `go get -u` to retrieve the latest version of the hdrhistogram.
```go
go get github.com/HdrHistogram/hdrhistogram-go
```
### Go Modules
If you are using Go modules, your `go get` will default to the latest tagged
release version of the histogram. To get a specific release version, use
`@<tag>` in your `go get` command.
```go
go get github.com/HdrHistogram/hdrhistogram-go@v0.9.0
```
To get the latest HdrHistogram/hdrhistogram-go master repository change use `@latest`.
```go
go get github.com/HdrHistogram/hdrhistogram-go@latest
```
### Repo transfer and impact on go dependencies
-------------------------------------------
This repository has been transferred under the github HdrHstogram umbrella with the help from the orginal
author in Sept 2020. The main reasons are to group all implementations under the same roof and to provide more active contribution
from the community as the orginal repository was archived several years ago.
Unfortunately such URL change will break go applications that depend on this library
directly or indirectly, as discussed [here](https://github.com/HdrHistogram/hdrhistogram-go/issues/30#issuecomment-696365251).
The dependency URL should be modified to point to the new repository URL.
The tag "v0.9.0" was applied at the point of transfer and will reflect the exact code that was frozen in the
original repository.
If you are using Go modules, you can update to the exact point of transfter using the `@v0.9.0` tag in your `go get` command.
```
go mod edit -replace github.com/codahale/hdrhistogram=github.com/HdrHistogram/hdrhistogram-go@v0.9.0
```
## Credits
-------
Many thanks for Coda Hale for contributing the initial implementation and transfering the repository here.

View File

@@ -1,782 +0,0 @@
// Package hdrhistogram provides an implementation of Gil Tene's HDR Histogram
// data structure. The HDR Histogram allows for fast and accurate analysis of
// the extreme ranges of data with non-normal distributions, like latency.
package hdrhistogram
import (
"fmt"
"io"
"math"
"math/bits"
"sort"
)
// A Bracket is a part of a cumulative distribution.
type Bracket struct {
Quantile float64
Count, ValueAt int64
}
// A Snapshot is an exported view of a Histogram, useful for serializing them.
// A Histogram can be constructed from it by passing it to Import.
type Snapshot struct {
LowestTrackableValue int64
HighestTrackableValue int64
SignificantFigures int64
Counts []int64
}
// A Histogram is a lossy data structure used to record the distribution of
// non-normally distributed data (like latency) with a high degree of accuracy
// and a bounded degree of precision.
type Histogram struct {
lowestDiscernibleValue int64
highestTrackableValue int64
unitMagnitude int64
significantFigures int64
subBucketHalfCountMagnitude int32
subBucketHalfCount int32
subBucketMask int64
subBucketCount int32
bucketCount int32
countsLen int32
totalCount int64
counts []int64
startTimeMs int64
endTimeMs int64
tag string
}
func (h *Histogram) Tag() string {
return h.tag
}
func (h *Histogram) SetTag(tag string) {
h.tag = tag
}
func (h *Histogram) EndTimeMs() int64 {
return h.endTimeMs
}
func (h *Histogram) SetEndTimeMs(endTimeMs int64) {
h.endTimeMs = endTimeMs
}
func (h *Histogram) StartTimeMs() int64 {
return h.startTimeMs
}
func (h *Histogram) SetStartTimeMs(startTimeMs int64) {
h.startTimeMs = startTimeMs
}
// Construct a Histogram given the Lowest and Highest values to be tracked and a number of significant decimal digits.
//
// Providing a lowestDiscernibleValue is useful in situations where the units used for the histogram's values are
// much smaller that the minimal accuracy required.
// E.g. when tracking time values stated in nanosecond units, where the minimal accuracy required is a microsecond,
// the proper value for lowestDiscernibleValue would be 1000.
//
// Note: the numberOfSignificantValueDigits must be [1,5]. If lower than 1 the numberOfSignificantValueDigits will be
// forced to 1, and if higher than 5 the numberOfSignificantValueDigits will be forced to 5.
func New(lowestDiscernibleValue, highestTrackableValue int64, numberOfSignificantValueDigits int) *Histogram {
if numberOfSignificantValueDigits < 1 {
numberOfSignificantValueDigits = 1
} else if numberOfSignificantValueDigits > 5 {
numberOfSignificantValueDigits = 5
}
if lowestDiscernibleValue < 1 {
lowestDiscernibleValue = 1
}
// Given a 3 decimal point accuracy, the expectation is obviously for "+/- 1 unit at 1000". It also means that
// it's "ok to be +/- 2 units at 2000". The "tricky" thing is that it is NOT ok to be +/- 2 units at 1999. Only
// starting at 2000. So internally, we need to maintain single unit resolution to 2x 10^decimalPoints.
largestValueWithSingleUnitResolution := 2 * math.Pow10(numberOfSignificantValueDigits)
// We need to maintain power-of-two subBucketCount (for clean direct indexing) that is large enough to
// provide unit resolution to at least largestValueWithSingleUnitResolution. So figure out
// largestValueWithSingleUnitResolution's nearest power-of-two (rounded up), and use that:
subBucketCountMagnitude := int32(math.Ceil(math.Log2(float64(largestValueWithSingleUnitResolution))))
subBucketHalfCountMagnitude := subBucketCountMagnitude
if subBucketHalfCountMagnitude < 1 {
subBucketHalfCountMagnitude = 1
}
subBucketHalfCountMagnitude--
unitMagnitude := int32(math.Floor(math.Log2(float64(lowestDiscernibleValue))))
if unitMagnitude < 0 {
unitMagnitude = 0
}
subBucketCount := int32(math.Pow(2, float64(subBucketHalfCountMagnitude)+1))
subBucketHalfCount := subBucketCount / 2
subBucketMask := int64(subBucketCount-1) << uint(unitMagnitude)
// determine exponent range needed to support the trackable value with no
// overflow:
smallestUntrackableValue := int64(subBucketCount) << uint(unitMagnitude)
bucketsNeeded := getBucketsNeededToCoverValue(smallestUntrackableValue, highestTrackableValue)
bucketCount := bucketsNeeded
countsLen := (bucketCount + 1) * (subBucketCount / 2)
return &Histogram{
lowestDiscernibleValue: lowestDiscernibleValue,
highestTrackableValue: highestTrackableValue,
unitMagnitude: int64(unitMagnitude),
significantFigures: int64(numberOfSignificantValueDigits),
subBucketHalfCountMagnitude: subBucketHalfCountMagnitude,
subBucketHalfCount: subBucketHalfCount,
subBucketMask: subBucketMask,
subBucketCount: subBucketCount,
bucketCount: bucketCount,
countsLen: countsLen,
totalCount: 0,
counts: make([]int64, countsLen),
startTimeMs: 0,
endTimeMs: 0,
tag: "",
}
}
func getBucketsNeededToCoverValue(smallestUntrackableValue int64, maxValue int64) int32 {
// always have at least 1 bucket
bucketsNeeded := int32(1)
for smallestUntrackableValue < maxValue {
if smallestUntrackableValue > (math.MaxInt64 / 2) {
// next shift will overflow, meaning that bucket could represent values up to ones greater than
// math.MaxInt64, so it's the last bucket
return bucketsNeeded + 1
}
smallestUntrackableValue <<= 1
bucketsNeeded++
}
return bucketsNeeded
}
// ByteSize returns an estimate of the amount of memory allocated to the
// histogram in bytes.
//
// N.B.: This does not take into account the overhead for slices, which are
// small, constant, and specific to the compiler version.
func (h *Histogram) ByteSize() int {
return 6*8 + 5*4 + len(h.counts)*8
}
func (h *Histogram) getNormalizingIndexOffset() int32 {
return 1
}
// Merge merges the data stored in the given histogram with the receiver,
// returning the number of recorded values which had to be dropped.
func (h *Histogram) Merge(from *Histogram) (dropped int64) {
i := from.rIterator()
for i.next() {
v := i.valueFromIdx
c := i.countAtIdx
if h.RecordValues(v, c) != nil {
dropped += c
}
}
return
}
// TotalCount returns total number of values recorded.
func (h *Histogram) TotalCount() int64 {
return h.totalCount
}
// Max returns the approximate maximum recorded value.
func (h *Histogram) Max() int64 {
var max int64
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 {
max = i.highestEquivalentValue
}
}
return h.highestEquivalentValue(max)
}
// Min returns the approximate minimum recorded value.
func (h *Histogram) Min() int64 {
var min int64
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 && min == 0 {
min = i.highestEquivalentValue
break
}
}
return h.lowestEquivalentValue(min)
}
// Mean returns the approximate arithmetic mean of the recorded values.
func (h *Histogram) Mean() float64 {
if h.totalCount == 0 {
return 0
}
var total int64
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 {
total += i.countAtIdx * h.medianEquivalentValue(i.valueFromIdx)
}
}
return float64(total) / float64(h.totalCount)
}
// StdDev returns the approximate standard deviation of the recorded values.
func (h *Histogram) StdDev() float64 {
if h.totalCount == 0 {
return 0
}
mean := h.Mean()
geometricDevTotal := 0.0
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 {
dev := float64(h.medianEquivalentValue(i.valueFromIdx)) - mean
geometricDevTotal += (dev * dev) * float64(i.countAtIdx)
}
}
return math.Sqrt(geometricDevTotal / float64(h.totalCount))
}
// Reset deletes all recorded values and restores the histogram to its original
// state.
func (h *Histogram) Reset() {
h.totalCount = 0
for i := range h.counts {
h.counts[i] = 0
}
}
// RecordValue records the given value, returning an error if the value is out
// of range.
func (h *Histogram) RecordValue(v int64) error {
return h.RecordValues(v, 1)
}
// RecordCorrectedValue records the given value, correcting for stalls in the
// recording process. This only works for processes which are recording values
// at an expected interval (e.g., doing jitter analysis). Processes which are
// recording ad-hoc values (e.g., latency for incoming requests) can't take
// advantage of this.
func (h *Histogram) RecordCorrectedValue(v, expectedInterval int64) error {
if err := h.RecordValue(v); err != nil {
return err
}
if expectedInterval <= 0 || v <= expectedInterval {
return nil
}
missingValue := v - expectedInterval
for missingValue >= expectedInterval {
if err := h.RecordValue(missingValue); err != nil {
return err
}
missingValue -= expectedInterval
}
return nil
}
// RecordValues records n occurrences of the given value, returning an error if
// the value is out of range.
func (h *Histogram) RecordValues(v, n int64) error {
idx := h.countsIndexFor(v)
if idx < 0 || int(h.countsLen) <= idx {
return fmt.Errorf("value %d is too large to be recorded", v)
}
h.setCountAtIndex(idx, n)
return nil
}
func (h *Histogram) setCountAtIndex(idx int, n int64) {
h.counts[idx] += n
h.totalCount += n
}
// ValueAtQuantile returns the largest value that (100% - percentile) of the overall recorded value entries
// in the histogram are either larger than or equivalent to.
//
// The passed quantile must be a float64 value in [0.0 .. 100.0]
// Note that two values are "equivalent" if `ValuesAreEquivalent(value1,value2)` would return true.
//
// Returns 0 if no recorded values exist.
func (h *Histogram) ValueAtQuantile(q float64) int64 {
return h.ValueAtPercentile(q)
}
// ValueAtPercentile returns the largest value that (100% - percentile) of the overall recorded value entries
// in the histogram are either larger than or equivalent to.
//
// The passed percentile must be a float64 value in [0.0 .. 100.0]
// Note that two values are "equivalent" if `ValuesAreEquivalent(value1,value2)` would return true.
//
// Returns 0 if no recorded values exist.
func (h *Histogram) ValueAtPercentile(percentile float64) int64 {
if percentile > 100 {
percentile = 100
}
countAtPercentile := int64(((percentile / 100) * float64(h.totalCount)) + 0.5)
valueFromIdx := h.getValueFromIdxUpToCount(countAtPercentile)
if percentile == 0.0 {
return h.lowestEquivalentValue(valueFromIdx)
}
return h.highestEquivalentValue(valueFromIdx)
}
func (h *Histogram) getValueFromIdxUpToCount(countAtPercentile int64) int64 {
var countToIdx int64
var valueFromIdx int64
var subBucketIdx int32 = -1
var bucketIdx int32
bucketBaseIdx := h.getBucketBaseIdx(bucketIdx)
for {
if countToIdx >= countAtPercentile {
break
}
// increment bucket
subBucketIdx++
if subBucketIdx >= h.subBucketCount {
subBucketIdx = h.subBucketHalfCount
bucketIdx++
bucketBaseIdx = h.getBucketBaseIdx(bucketIdx)
}
countToIdx += h.getCountAtIndexGivenBucketBaseIdx(bucketBaseIdx, subBucketIdx)
valueFromIdx = int64(subBucketIdx) << uint(int64(bucketIdx)+h.unitMagnitude)
}
return valueFromIdx
}
// ValueAtPercentiles, given an slice of percentiles returns a map containing for each passed percentile,
// the largest value that (100% - percentile) of the overall recorded value entries
// in the histogram are either larger than or equivalent to.
//
// Each element in the given an slice of percentiles must be a float64 value in [0.0 .. 100.0]
// Note that two values are "equivalent" if `ValuesAreEquivalent(value1,value2)` would return true.
//
// Returns a map of 0's if no recorded values exist.
func (h *Histogram) ValueAtPercentiles(percentiles []float64) (values map[float64]int64) {
sort.Float64s(percentiles)
totalQuantilesToCalculate := len(percentiles)
values = make(map[float64]int64, totalQuantilesToCalculate)
countAtPercentiles := make([]int64, totalQuantilesToCalculate)
for i, percentile := range percentiles {
if percentile > 100 {
percentile = 100
}
values[percentile] = 0
countAtPercentiles[i] = int64(((percentile / 100) * float64(h.totalCount)) + 0.5)
}
total := int64(0)
currentQuantileSlicePos := 0
i := h.iterator()
for currentQuantileSlicePos < totalQuantilesToCalculate && i.nextCountAtIdx(h.totalCount) {
total += i.countAtIdx
for currentQuantileSlicePos < totalQuantilesToCalculate && total >= countAtPercentiles[currentQuantileSlicePos] {
currentPercentile := percentiles[currentQuantileSlicePos]
if currentPercentile == 0.0 {
values[currentPercentile] = h.lowestEquivalentValue(i.valueFromIdx)
} else {
values[currentPercentile] = h.highestEquivalentValue(i.valueFromIdx)
}
currentQuantileSlicePos++
}
}
return
}
// Determine if two values are equivalent with the histogram's resolution.
// Where "equivalent" means that value samples recorded for any two
// equivalent values are counted in a common total count.
func (h *Histogram) ValuesAreEquivalent(value1, value2 int64) (result bool) {
result = h.lowestEquivalentValue(value1) == h.lowestEquivalentValue(value2)
return
}
// CumulativeDistribution returns an ordered list of brackets of the
// distribution of recorded values.
func (h *Histogram) CumulativeDistribution() []Bracket {
var result []Bracket
i := h.pIterator(1)
for i.next() {
result = append(result, Bracket{
Quantile: i.percentile,
Count: i.countToIdx,
ValueAt: i.highestEquivalentValue,
})
}
return result
}
// SignificantFigures returns the significant figures used to create the
// histogram
func (h *Histogram) SignificantFigures() int64 {
return h.significantFigures
}
// LowestTrackableValue returns the lower bound on values that will be added
// to the histogram
func (h *Histogram) LowestTrackableValue() int64 {
return h.lowestDiscernibleValue
}
// HighestTrackableValue returns the upper bound on values that will be added
// to the histogram
func (h *Histogram) HighestTrackableValue() int64 {
return h.highestTrackableValue
}
// Histogram bar for plotting
type Bar struct {
From, To, Count int64
}
// Pretty print as csv for easy plotting
func (b Bar) String() string {
return fmt.Sprintf("%v, %v, %v\n", b.From, b.To, b.Count)
}
// Distribution returns an ordered list of bars of the
// distribution of recorded values, counts can be normalized to a probability
func (h *Histogram) Distribution() (result []Bar) {
i := h.iterator()
for i.next() {
result = append(result, Bar{
Count: i.countAtIdx,
From: h.lowestEquivalentValue(i.valueFromIdx),
To: i.highestEquivalentValue,
})
}
return result
}
// Equals returns true if the two Histograms are equivalent, false if not.
func (h *Histogram) Equals(other *Histogram) bool {
switch {
case
h.lowestDiscernibleValue != other.lowestDiscernibleValue,
h.highestTrackableValue != other.highestTrackableValue,
h.unitMagnitude != other.unitMagnitude,
h.significantFigures != other.significantFigures,
h.subBucketHalfCountMagnitude != other.subBucketHalfCountMagnitude,
h.subBucketHalfCount != other.subBucketHalfCount,
h.subBucketMask != other.subBucketMask,
h.subBucketCount != other.subBucketCount,
h.bucketCount != other.bucketCount,
h.countsLen != other.countsLen,
h.totalCount != other.totalCount:
return false
default:
for i, c := range h.counts {
if c != other.counts[i] {
return false
}
}
}
return true
}
// Export returns a snapshot view of the Histogram. This can be later passed to
// Import to construct a new Histogram with the same state.
func (h *Histogram) Export() *Snapshot {
return &Snapshot{
LowestTrackableValue: h.lowestDiscernibleValue,
HighestTrackableValue: h.highestTrackableValue,
SignificantFigures: h.significantFigures,
Counts: append([]int64(nil), h.counts...), // copy
}
}
// Import returns a new Histogram populated from the Snapshot data (which the
// caller must stop accessing).
func Import(s *Snapshot) *Histogram {
h := New(s.LowestTrackableValue, s.HighestTrackableValue, int(s.SignificantFigures))
h.counts = s.Counts
totalCount := int64(0)
for i := int32(0); i < h.countsLen; i++ {
countAtIndex := h.counts[i]
if countAtIndex > 0 {
totalCount += countAtIndex
}
}
h.totalCount = totalCount
return h
}
func (h *Histogram) iterator() *iterator {
return &iterator{
h: h,
subBucketIdx: -1,
}
}
func (h *Histogram) rIterator() *rIterator {
return &rIterator{
iterator: iterator{
h: h,
subBucketIdx: -1,
},
}
}
func (h *Histogram) pIterator(ticksPerHalfDistance int32) *pIterator {
return &pIterator{
iterator: iterator{
h: h,
subBucketIdx: -1,
},
ticksPerHalfDistance: ticksPerHalfDistance,
}
}
func (h *Histogram) sizeOfEquivalentValueRange(v int64) int64 {
bucketIdx := h.getBucketIndex(v)
return h.sizeOfEquivalentValueRangeGivenBucketIdx(v, bucketIdx)
}
func (h *Histogram) sizeOfEquivalentValueRangeGivenBucketIdx(v int64, bucketIdx int32) int64 {
subBucketIdx := h.getSubBucketIdx(v, bucketIdx)
adjustedBucket := bucketIdx
if subBucketIdx >= h.subBucketCount {
adjustedBucket++
}
return int64(1) << uint(h.unitMagnitude+int64(adjustedBucket))
}
func (h *Histogram) valueFromIndex(bucketIdx, subBucketIdx int32) int64 {
return int64(subBucketIdx) << uint(int64(bucketIdx)+h.unitMagnitude)
}
func (h *Histogram) lowestEquivalentValue(v int64) int64 {
bucketIdx := h.getBucketIndex(v)
return h.lowestEquivalentValueGivenBucketIdx(v, bucketIdx)
}
func (h *Histogram) lowestEquivalentValueGivenBucketIdx(v int64, bucketIdx int32) int64 {
subBucketIdx := h.getSubBucketIdx(v, bucketIdx)
return h.valueFromIndex(bucketIdx, subBucketIdx)
}
func (h *Histogram) nextNonEquivalentValue(v int64) int64 {
bucketIdx := h.getBucketIndex(v)
return h.lowestEquivalentValueGivenBucketIdx(v, bucketIdx) + h.sizeOfEquivalentValueRangeGivenBucketIdx(v, bucketIdx)
}
func (h *Histogram) highestEquivalentValue(v int64) int64 {
return h.nextNonEquivalentValue(v) - 1
}
func (h *Histogram) medianEquivalentValue(v int64) int64 {
return h.lowestEquivalentValue(v) + (h.sizeOfEquivalentValueRange(v) >> 1)
}
func (h *Histogram) getCountAtIndex(bucketIdx, subBucketIdx int32) int64 {
return h.counts[h.countsIndex(bucketIdx, subBucketIdx)]
}
func (h *Histogram) getCountAtIndexGivenBucketBaseIdx(bucketBaseIdx, subBucketIdx int32) int64 {
return h.counts[bucketBaseIdx+subBucketIdx-h.subBucketHalfCount]
}
func (h *Histogram) countsIndex(bucketIdx, subBucketIdx int32) int32 {
return h.getBucketBaseIdx(bucketIdx) + subBucketIdx - h.subBucketHalfCount
}
func (h *Histogram) getBucketBaseIdx(bucketIdx int32) int32 {
return (bucketIdx + 1) << uint(h.subBucketHalfCountMagnitude)
}
// return the lowest (and therefore highest precision) bucket index that can represent the value
// Calculates the number of powers of two by which the value is greater than the biggest value that fits in
// bucket 0. This is the bucket index since each successive bucket can hold a value 2x greater.
func (h *Histogram) getBucketIndex(v int64) int32 {
var pow2Ceiling = int64(64 - bits.LeadingZeros64(uint64(v|h.subBucketMask)))
return int32(pow2Ceiling - int64(h.unitMagnitude) -
int64(h.subBucketHalfCountMagnitude+1))
}
// For bucketIndex 0, this is just value, so it may be anywhere in 0 to subBucketCount.
// For other bucketIndex, this will always end up in the top half of subBucketCount: assume that for some bucket
// k > 0, this calculation will yield a value in the bottom half of 0 to subBucketCount. Then, because of how
// buckets overlap, it would have also been in the top half of bucket k-1, and therefore would have
// returned k-1 in getBucketIndex(). Since we would then shift it one fewer bits here, it would be twice as big,
// and therefore in the top half of subBucketCount.
func (h *Histogram) getSubBucketIdx(v int64, idx int32) int32 {
return int32(v >> uint(int64(idx)+int64(h.unitMagnitude)))
}
func (h *Histogram) countsIndexFor(v int64) int {
bucketIdx := h.getBucketIndex(v)
subBucketIdx := h.getSubBucketIdx(v, bucketIdx)
return int(h.countsIndex(bucketIdx, subBucketIdx))
}
func (h *Histogram) getIntegerToDoubleValueConversionRatio() float64 {
return 1.0
}
type iterator struct {
h *Histogram
bucketIdx, subBucketIdx int32
countAtIdx, countToIdx, valueFromIdx int64
highestEquivalentValue int64
}
// nextCountAtIdx does not update the iterator highestEquivalentValue in order to optimize cpu usage.
func (i *iterator) nextCountAtIdx(limit int64) bool {
if i.countToIdx >= limit {
return false
}
// increment bucket
i.subBucketIdx++
if i.subBucketIdx >= i.h.subBucketCount {
i.subBucketIdx = i.h.subBucketHalfCount
i.bucketIdx++
}
if i.bucketIdx >= i.h.bucketCount {
return false
}
i.countAtIdx = i.h.getCountAtIndex(i.bucketIdx, i.subBucketIdx)
i.countToIdx += i.countAtIdx
i.valueFromIdx = i.h.valueFromIndex(i.bucketIdx, i.subBucketIdx)
return true
}
// Returns the next element in the iteration.
func (i *iterator) next() bool {
if !i.nextCountAtIdx(i.h.totalCount) {
return false
}
i.highestEquivalentValue = i.h.highestEquivalentValue(i.valueFromIdx)
return true
}
type rIterator struct {
iterator
countAddedThisStep int64
}
func (r *rIterator) next() bool {
for r.iterator.next() {
if r.countAtIdx != 0 {
r.countAddedThisStep = r.countAtIdx
return true
}
}
return false
}
type pIterator struct {
iterator
seenLastValue bool
ticksPerHalfDistance int32
percentileToIteratorTo float64
percentile float64
}
func (p *pIterator) next() bool {
if !(p.countToIdx < p.h.totalCount) {
if p.seenLastValue {
return false
}
p.seenLastValue = true
p.percentile = 100
return true
}
if p.subBucketIdx == -1 && !p.iterator.next() {
return false
}
var done = false
for !done {
currentPercentile := (100.0 * float64(p.countToIdx)) / float64(p.h.totalCount)
if p.countAtIdx != 0 && p.percentileToIteratorTo <= currentPercentile {
p.percentile = p.percentileToIteratorTo
halfDistance := math.Trunc(math.Pow(2, math.Trunc(math.Log2(100.0/(100.0-p.percentileToIteratorTo)))+1))
percentileReportingTicks := float64(p.ticksPerHalfDistance) * halfDistance
p.percentileToIteratorTo += 100.0 / percentileReportingTicks
return true
}
done = !p.iterator.next()
}
return true
}
// CumulativeDistribution returns an ordered list of brackets of the
// distribution of recorded values.
func (h *Histogram) CumulativeDistributionWithTicks(ticksPerHalfDistance int32) []Bracket {
var result []Bracket
i := h.pIterator(ticksPerHalfDistance)
for i.next() {
result = append(result, Bracket{
Quantile: i.percentile,
Count: i.countToIdx,
ValueAt: int64(i.highestEquivalentValue),
})
}
return result
}
// Output the percentiles distribution in a text format
func (h *Histogram) PercentilesPrint(writer io.Writer, ticksPerHalfDistance int32, valueScale float64) (outputWriter io.Writer, err error) {
outputWriter = writer
dist := h.CumulativeDistributionWithTicks(ticksPerHalfDistance)
_, err = outputWriter.Write([]byte(" Value\tPercentile\tTotalCount\t1/(1-Percentile)\n\n"))
if err != nil {
return
}
for _, slice := range dist {
percentile := slice.Quantile / 100.0
inverted_percentile := 1.0 / (1.0 - percentile)
var inverted_percentile_string = fmt.Sprintf("%12.2f", inverted_percentile)
// Given that other language implementations display inf (instead of Go's +Inf)
// we want to be as close as possible to them
if math.IsInf(inverted_percentile, 1) {
inverted_percentile_string = fmt.Sprintf("%12s", "inf")
}
_, err = outputWriter.Write([]byte(fmt.Sprintf("%12.3f %12f %12d %s\n", float64(slice.ValueAt)/valueScale, percentile, slice.Count, inverted_percentile_string)))
if err != nil {
return
}
}
footer := fmt.Sprintf("#[Mean = %12.3f, StdDeviation = %12.3f]\n#[Max = %12.3f, Total count = %12d]\n#[Buckets = %12d, SubBuckets = %12d]\n",
h.Mean()/valueScale,
h.StdDev()/valueScale,
float64(h.Max())/valueScale,
h.TotalCount(),
h.bucketCount,
h.subBucketCount,
)
_, err = outputWriter.Write([]byte(footer))
return
}

View File

@@ -1,261 +0,0 @@
// Histograms are encoded using the HdrHistogram V2 format which is based on an adapted ZigZag LEB128 encoding where:
// consecutive zero counters are encoded as a negative number representing the count of consecutive zeros
// non zero counter values are encoded as a positive number
// A typical histogram (2 digits precision 1 usec to 1 day range) can be encoded in less than the typical MTU size of 1500 bytes.
package hdrhistogram
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/binary"
"fmt"
"io/ioutil"
)
const (
V2EncodingCookieBase int32 = 0x1c849303
V2CompressedEncodingCookieBase int32 = 0x1c849304
encodingCookie int32 = V2EncodingCookieBase | 0x10
compressedEncodingCookie int32 = V2CompressedEncodingCookieBase | 0x10
ENCODING_HEADER_SIZE = 40
)
// Encode returns a snapshot view of the Histogram.
// The snapshot is compact binary representations of the state of the histogram.
// They are intended to be used for archival or transmission to other systems for further analysis.
func (h *Histogram) Encode(version int32) (buffer []byte, err error) {
switch version {
case V2CompressedEncodingCookieBase:
buffer, err = h.dumpV2CompressedEncoding()
default:
err = fmt.Errorf("The provided enconding version %d is not supported.", version)
}
return
}
// Decode returns a new Histogram by decoding it from a String containing
// a base64 encoded compressed histogram representation.
func Decode(encoded []byte) (rh *Histogram, err error) {
var decoded []byte
decoded, err = base64.StdEncoding.DecodeString(string(encoded))
if err != nil {
return
}
rbuf := bytes.NewBuffer(decoded[0:8])
r32 := make([]int32, 2)
err = binary.Read(rbuf, binary.BigEndian, &r32)
if err != nil {
return
}
Cookie := r32[0] & ^0xf0
lengthOfCompressedContents := r32[1]
if Cookie != V2CompressedEncodingCookieBase {
err = fmt.Errorf("Encoding not supported, only V2 is supported. Got %d want %d", Cookie, V2CompressedEncodingCookieBase)
return
}
decodeLengthOfCompressedContents := int32(len(decoded[8:]))
if lengthOfCompressedContents > decodeLengthOfCompressedContents {
err = fmt.Errorf("The compressed contents buffer is smaller than the lengthOfCompressedContents. Got %d want %d", decodeLengthOfCompressedContents, lengthOfCompressedContents)
return
}
rh, err = decodeCompressedFormat(decoded[8:8+lengthOfCompressedContents], ENCODING_HEADER_SIZE)
return
}
// internal method to encode an histogram in V2 Compressed format
func (h *Histogram) dumpV2CompressedEncoding() (outBuffer []byte, err error) {
// final buffer
buf := new(bytes.Buffer)
err = binary.Write(buf, binary.BigEndian, compressedEncodingCookie)
if err != nil {
return
}
toCompress, err := h.encodeIntoByteBuffer()
if err != nil {
return
}
uncompressedBytes := toCompress.Bytes()
var b bytes.Buffer
w, err := zlib.NewWriterLevel(&b, zlib.BestCompression)
if err != nil {
return
}
_, err = w.Write(uncompressedBytes)
if err != nil {
return
}
w.Close()
// LengthOfCompressedContents
compressedContents := b.Bytes()
err = binary.Write(buf, binary.BigEndian, int32(len(compressedContents)))
if err != nil {
return
}
err = binary.Write(buf, binary.BigEndian, compressedContents)
if err != nil {
return
}
outBuffer = []byte(base64.StdEncoding.EncodeToString(buf.Bytes()))
return
}
func (h *Histogram) encodeIntoByteBuffer() (*bytes.Buffer, error) {
countsBytes, err := h.fillBufferFromCountsArray()
if err != nil {
return nil, err
}
toCompress := new(bytes.Buffer)
err = binary.Write(toCompress, binary.BigEndian, encodingCookie) // 0-3
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, int32(len(countsBytes))) // 3-7
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.getNormalizingIndexOffset()) // 8-11
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, int32(h.significantFigures)) // 12-15
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.lowestDiscernibleValue) // 16-23
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.highestTrackableValue) // 24-31
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.getIntegerToDoubleValueConversionRatio()) // 32-39
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, countsBytes)
if err != nil {
return nil, err
}
return toCompress, err
}
func decodeCompressedFormat(compressedContents []byte, headerSize int) (rh *Histogram, err error) {
b := bytes.NewReader(compressedContents)
z, err := zlib.NewReader(b)
if err != nil {
return
}
defer z.Close()
decompressedSlice, err := ioutil.ReadAll(z)
if err != nil {
return
}
decompressedSliceLen := int32(len(decompressedSlice))
cookie, PayloadLength, _, NumberOfSignificantValueDigits, LowestTrackableValue, HighestTrackableValue, _, err := decodeDeCompressedHeaderFormat(decompressedSlice[0:headerSize])
if err != nil {
return
}
if cookie != V2EncodingCookieBase {
err = fmt.Errorf("Encoding not supported, only V2 is supported. Got %d want %d", cookie, V2EncodingCookieBase)
return
}
actualPayloadLen := decompressedSliceLen - int32(headerSize)
if PayloadLength != actualPayloadLen {
err = fmt.Errorf("PayloadLength should have the same size of the actual payload. Got %d want %d", actualPayloadLen, PayloadLength)
return
}
rh = New(LowestTrackableValue, HighestTrackableValue, int(NumberOfSignificantValueDigits))
payload := decompressedSlice[headerSize:]
err = fillCountsArrayFromSourceBuffer(payload, rh)
return rh, err
}
func fillCountsArrayFromSourceBuffer(payload []byte, rh *Histogram) (err error) {
var payloadSlicePos = 0
var dstIndex int64 = 0
var n int
var count int64
var zerosCount int64
for payloadSlicePos < len(payload) {
count, n, err = zig_zag_decode_i64(payload[payloadSlicePos:])
if err != nil {
return
}
payloadSlicePos += n
if count < 0 {
zerosCount = -count
dstIndex += zerosCount
} else {
rh.setCountAtIndex(int(dstIndex), count)
dstIndex += 1
}
}
return
}
func (rh *Histogram) fillBufferFromCountsArray() (buffer []byte, err error) {
buf := new(bytes.Buffer)
// V2 encoding format uses a ZigZag LEB128-64b9B encoded long. Positive values are counts,
// while negative values indicate a repeat zero counts.
var countsLimit int32 = int32(rh.countsIndexFor(rh.Max()) + 1)
var srcIndex int32 = 0
for srcIndex < countsLimit {
count := rh.counts[srcIndex]
srcIndex++
var zeros int64 = 0
// check for contiguous zeros
if count == 0 {
zeros = 1
for srcIndex < countsLimit && 0 == rh.counts[srcIndex] {
zeros++
srcIndex++
}
}
if zeros > 1 {
err = binary.Write(buf, binary.BigEndian, zig_zag_encode_i64(-zeros))
if err != nil {
return
}
} else {
err = binary.Write(buf, binary.BigEndian, zig_zag_encode_i64(count))
if err != nil {
return
}
}
}
buffer = buf.Bytes()
return
}
func decodeDeCompressedHeaderFormat(decoded []byte) (Cookie int32, PayloadLength int32, NormalizingIndexOffSet int32, NumberOfSignificantValueDigits int32, LowestTrackableValue int64, HighestTrackableValue int64, IntegerToDoubleConversionRatio float64, err error) {
rbuf := bytes.NewBuffer(decoded[0:40])
r32 := make([]int32, 4)
r64 := make([]int64, 2)
err = binary.Read(rbuf, binary.BigEndian, &r32)
if err != nil {
return
}
err = binary.Read(rbuf, binary.BigEndian, &r64)
if err != nil {
return
}
err = binary.Read(rbuf, binary.BigEndian, &IntegerToDoubleConversionRatio)
if err != nil {
return
}
Cookie = r32[0] & ^0xf0
PayloadLength = r32[1]
NormalizingIndexOffSet = r32[2]
NumberOfSignificantValueDigits = r32[3]
LowestTrackableValue = r64[0]
HighestTrackableValue = r64[1]
return
}

View File

@@ -1,209 +0,0 @@
package hdrhistogram
import (
"bufio"
"io"
"math"
"regexp"
"strconv"
"strings"
)
type HistogramLogReader struct {
log *bufio.Reader
startTimeSec float64
observedStartTime bool
baseTimeSec float64
observedBaseTime bool
// scanner handling state
absolute bool
rangeStartTimeSec float64
rangeEndTimeSec float64
observedMax bool
rangeObservedMax int64
observedMin bool
rangeObservedMin int64
reStartTime *regexp.Regexp
reBaseTime *regexp.Regexp
reHistogramInterval *regexp.Regexp
}
func (hlr *HistogramLogReader) ObservedMin() bool {
return hlr.observedMin
}
func (hlr *HistogramLogReader) ObservedMax() bool {
return hlr.observedMax
}
// Returns the overall observed max limit ( up to the current point ) of the read histograms
func (hlr *HistogramLogReader) RangeObservedMax() int64 {
return hlr.rangeObservedMax
}
// Returns the overall observed min limit ( up to the current point ) of the read histograms
func (hlr *HistogramLogReader) RangeObservedMin() int64 {
return hlr.rangeObservedMin
}
func NewHistogramLogReader(log io.Reader) *HistogramLogReader {
//# "#[StartTime: %f (seconds since epoch), %s]\n"
reStartTime, _ := regexp.Compile(`#\[StartTime: ([\d\.]*)`)
//# "#[BaseTime: %f (seconds since epoch)]\n"
reBaseTime, _ := regexp.Compile(`#\[BaseTime: ([\d\.]*)`)
//# 0.127,1.007,2.769,HISTFAAAAEV42pNpmSz...
//# Tag=A,0.127,1.007,2.769,HISTFAAAAEV42pNpmSz
//# "%f,%f,%f,%s\n"
reHistogramInterval, _ := regexp.Compile(`([\d\.]*),([\d\.]*),([\d\.]*),(.*)`)
//
reader := bufio.NewReader(log)
return &HistogramLogReader{log: reader,
startTimeSec: 0.0,
observedStartTime: false,
baseTimeSec: 0.0,
observedBaseTime: false,
reStartTime: reStartTime,
reBaseTime: reBaseTime,
reHistogramInterval: reHistogramInterval,
rangeObservedMin: math.MaxInt64,
observedMin: false,
rangeObservedMax: math.MinInt64,
observedMax: false,
}
}
func (hlr *HistogramLogReader) NextIntervalHistogram() (histogram *Histogram, err error) {
return hlr.NextIntervalHistogramWithRange(0.0, math.MaxFloat64, true)
}
func (hlr *HistogramLogReader) NextIntervalHistogramWithRange(rangeStartTimeSec, rangeEndTimeSec float64, absolute bool) (histogram *Histogram, err error) {
hlr.rangeStartTimeSec = rangeStartTimeSec
hlr.rangeEndTimeSec = rangeEndTimeSec
hlr.absolute = absolute
return hlr.decodeNextIntervalHistogram()
}
func (hlr *HistogramLogReader) decodeNextIntervalHistogram() (histogram *Histogram, err error) {
var line string
var tag string = ""
var logTimeStampInSec float64
var intervalLengthSec float64
for {
line, err = hlr.log.ReadString('\n')
if err != nil {
if err == io.EOF {
err = nil
break
}
break
}
if line[0] == '#' {
matchRes := hlr.reStartTime.FindStringSubmatch(line)
if len(matchRes) > 0 {
hlr.startTimeSec, err = strconv.ParseFloat(matchRes[1], 64)
if err != nil {
return
}
hlr.observedStartTime = true
continue
}
matchRes = hlr.reBaseTime.FindStringSubmatch(line)
if len(matchRes) > 0 {
hlr.baseTimeSec, err = strconv.ParseFloat(matchRes[1], 64)
if err != nil {
return
}
hlr.observedBaseTime = true
continue
}
continue
}
if strings.HasPrefix(line, "Tag=") {
commaPos := strings.Index(line, ",")
tag = line[4:commaPos]
line = line[commaPos+1:]
}
matchRes := hlr.reHistogramInterval.FindStringSubmatch(line)
if len(matchRes) >= 1 {
// Decode: startTimestamp, intervalLength, maxTime, histogramPayload
// Timestamp is expected to be in seconds
logTimeStampInSec, err = strconv.ParseFloat(matchRes[1], 64)
if err != nil {
return
}
intervalLengthSec, err = strconv.ParseFloat(matchRes[2], 64)
if err != nil {
return
}
cpayload := matchRes[4]
// No explicit start time noted. Use 1st observed time:
if !hlr.observedStartTime {
hlr.startTimeSec = logTimeStampInSec
hlr.observedStartTime = true
}
// No explicit base time noted.
// Deduce from 1st observed time (compared to start time):
if !hlr.observedBaseTime {
// Criteria Note: if log timestamp is more than a year in
// the past (compared to StartTime),
// we assume that timestamps in the log are not absolute
if logTimeStampInSec < (hlr.startTimeSec - (365 * 24 * 3600.0)) {
hlr.baseTimeSec = hlr.startTimeSec
} else {
hlr.baseTimeSec = 0.0
}
hlr.observedBaseTime = true
}
absoluteStartTimeStampSec := logTimeStampInSec + hlr.baseTimeSec
offsetStartTimeStampSec := absoluteStartTimeStampSec + hlr.startTimeSec
// Timestamp length is expect to be in seconds
absoluteEndTimeStampSec := absoluteStartTimeStampSec + intervalLengthSec
var startTimeStampToCheckRangeOn float64
if hlr.absolute {
startTimeStampToCheckRangeOn = absoluteStartTimeStampSec
} else {
startTimeStampToCheckRangeOn = offsetStartTimeStampSec
}
if startTimeStampToCheckRangeOn < hlr.rangeStartTimeSec {
continue
}
if startTimeStampToCheckRangeOn > hlr.rangeEndTimeSec {
return
}
histogram, err = Decode([]byte(cpayload))
if err != nil {
return
}
if histogram.Max() > hlr.rangeObservedMax {
hlr.rangeObservedMax = histogram.Max()
}
if histogram.Min() < hlr.rangeObservedMin {
hlr.rangeObservedMin = histogram.Min()
}
histogram.SetStartTimeMs(int64(absoluteStartTimeStampSec * 1000.0))
histogram.SetEndTimeMs(int64(absoluteEndTimeStampSec * 1000.0))
if tag != "" {
histogram.SetTag(tag)
}
return
}
}
return
}

View File

@@ -1,141 +0,0 @@
//The log format encodes into a single file, multiple histograms with optional shared meta data.
package hdrhistogram
import (
"fmt"
"io"
"regexp"
"time"
)
const HISTOGRAM_LOG_FORMAT_VERSION = "1.3"
const MsToNsRatio float64 = 1000000.0
type HistogramLogOptions struct {
startTimeStampSec float64
endTimeStampSec float64
maxValueUnitRatio float64
}
func DefaultHistogramLogOptions() *HistogramLogOptions {
return &HistogramLogOptions{0, 0, MsToNsRatio}
}
type HistogramLogWriter struct {
baseTime int64
log io.Writer
}
// Return the current base time offset
func (lw *HistogramLogWriter) BaseTime() int64 {
return lw.baseTime
}
// Set a base time to subtract from supplied histogram start/end timestamps when
// logging based on histogram timestamps.
// baseTime is expected to be in msec since the epoch, as histogram start/end times
// are typically stamped with absolute times in msec since the epoch.
func (lw *HistogramLogWriter) SetBaseTime(baseTime int64) {
lw.baseTime = baseTime
}
func NewHistogramLogWriter(log io.Writer) *HistogramLogWriter {
return &HistogramLogWriter{baseTime: 0, log: log}
}
// Output an interval histogram, using the start/end timestamp indicated in the histogram, and the [optional] tag associated with the histogram.
// The histogram start and end timestamps are assumed to be in msec units
//
// By convention, histogram start/end time are generally stamped with absolute times in msec
// since the epoch. For logging with absolute time stamps, the base time would remain zero ( default ).
// For logging with relative time stamps (time since a start point), the base time should be set with SetBaseTime(baseTime int64)
//
// The max value in the histogram will be reported scaled down by a default maxValueUnitRatio of 1000000.0 (which is the msec : nsec ratio).
// If you need to specify a different start/end timestamp or a different maxValueUnitRatio you should use OutputIntervalHistogramWithLogOptions(histogram *Histogram, logOptions *HistogramLogOptions)
func (lw *HistogramLogWriter) OutputIntervalHistogram(histogram *Histogram) (err error) {
return lw.OutputIntervalHistogramWithLogOptions(histogram, nil)
}
// Output an interval histogram, with the given timestamp information and the [optional] tag associated with the histogram
//
// If you specify non-nil logOptions, and non-zero start timestamp, the the specified timestamp information will be used, and the start timestamp information in the actual histogram will be ignored.
// If you specify non-nil logOptions, and non-zero start timestamp, the the specified timestamp information will be used, and the end timestamp information in the actual histogram will be ignored.
// If you specify non-nil logOptions, The max value reported with the interval line will be scaled by the given maxValueUnitRatio,
// otherwise a default maxValueUnitRatio of 1,000,000 (which is the msec : nsec ratio) will be used.
//
// By convention, histogram start/end time are generally stamped with absolute times in msec
// since the epoch. For logging with absolute time stamps, the base time would remain zero ( default ).
// For logging with relative time stamps (time since a start point), the base time should be set with SetBaseTime(baseTime int64)
func (lw *HistogramLogWriter) OutputIntervalHistogramWithLogOptions(histogram *Histogram, logOptions *HistogramLogOptions) (err error) {
tag := histogram.Tag()
var match bool
tagStr := ""
if tag != "" {
match, err = regexp.MatchString(".[, \\r\\n].", tag)
if err != nil {
return
}
if match {
err = fmt.Errorf("Tag string cannot contain commas, spaces, or line breaks. Used tag: %s", tag)
return
}
tagStr = fmt.Sprintf("Tag=%s,", tag)
}
var usedStartTime float64 = float64(histogram.StartTimeMs())
var usedEndTime float64 = float64(histogram.EndTimeMs())
var maxValueUnitRatio float64 = MsToNsRatio
if logOptions != nil {
if logOptions.startTimeStampSec != 0 {
usedStartTime = logOptions.startTimeStampSec
}
if logOptions.endTimeStampSec != 0 {
usedEndTime = logOptions.endTimeStampSec
}
maxValueUnitRatio = logOptions.maxValueUnitRatio
}
startTime := usedStartTime - float64(lw.baseTime)/1000.0
endTime := usedEndTime - float64(lw.baseTime)/1000.0
maxValueAsDouble := float64(histogram.Max()) / maxValueUnitRatio
cpayload, err := histogram.Encode(V2CompressedEncodingCookieBase)
if err != nil {
return
}
_, err = lw.log.Write([]byte(fmt.Sprintf("%s%f,%f,%f,%s\n", tagStr, startTime, endTime, maxValueAsDouble, string(cpayload))))
return
}
// Log a start time in the log.
// Start time is represented as seconds since epoch with up to 3 decimal places. Line starts with the leading text '#[StartTime:'
func (lw *HistogramLogWriter) OutputStartTime(start_time_msec int64) (err error) {
secs := start_time_msec / 1000
iso_str := time.Unix(secs, start_time_msec%int64(1000)*int64(1000000000)).Format(time.RFC3339)
_, err = lw.log.Write([]byte(fmt.Sprintf("#[StartTime: %d (seconds since epoch), %s]\n", secs, iso_str)))
return
}
// Log a base time in the log.
// Base time is represented as seconds since epoch with up to 3 decimal places. Line starts with the leading text '#[BaseTime:'
func (lw *HistogramLogWriter) OutputBaseTime(base_time_msec int64) (err error) {
secs := base_time_msec / 1000
_, err = lw.log.Write([]byte(fmt.Sprintf("#[Basetime: %d (seconds since epoch)]\n", secs)))
return
}
// Log a comment to the log.
// A comment is any line that leads with '#' that is not matched by the BaseTime or StartTime formats. Comments are ignored when parsed.
func (lw *HistogramLogWriter) OutputComment(comment string) (err error) {
_, err = lw.log.Write([]byte(fmt.Sprintf("#%s\n", comment)))
return
}
// Output a legend line to the log.
// Human readable column headers. Ignored when parsed.
func (lw *HistogramLogWriter) OutputLegend() (err error) {
_, err = lw.log.Write([]byte("\"StartTimestamp\",\"Interval_Length\",\"Interval_Max\",\"Interval_Compressed_Histogram\"\n"))
return
}
// Output a log format version to the log.
func (lw *HistogramLogWriter) OutputLogFormatVersion() (err error) {
return lw.OutputComment(fmt.Sprintf("[Histogram log format version %s]", HISTOGRAM_LOG_FORMAT_VERSION))
}

View File

@@ -1,45 +0,0 @@
package hdrhistogram
// A WindowedHistogram combines histograms to provide windowed statistics.
type WindowedHistogram struct {
idx int
h []Histogram
m *Histogram
Current *Histogram
}
// NewWindowed creates a new WindowedHistogram with N underlying histograms with
// the given parameters.
func NewWindowed(n int, minValue, maxValue int64, sigfigs int) *WindowedHistogram {
w := WindowedHistogram{
idx: -1,
h: make([]Histogram, n),
m: New(minValue, maxValue, sigfigs),
}
for i := range w.h {
w.h[i] = *New(minValue, maxValue, sigfigs)
}
w.Rotate()
return &w
}
// Merge returns a histogram which includes the recorded values from all the
// sections of the window.
func (w *WindowedHistogram) Merge() *Histogram {
w.m.Reset()
for _, h := range w.h {
w.m.Merge(&h)
}
return w.m
}
// Rotate resets the oldest histogram and rotates it to be used as the current
// histogram.
func (w *WindowedHistogram) Rotate() {
w.idx++
w.Current = &w.h[w.idx%len(w.h)]
w.Current.Reset()
}

View File

@@ -1,133 +0,0 @@
package hdrhistogram
import "fmt"
const truncatedErrStr = "Truncated compressed histogram decode. Expected minimum length of %d bytes and got %d."
// Read an LEB128 ZigZag encoded long value from the given buffer
func zig_zag_decode_i64(buf []byte) (signedValue int64, n int, err error) {
buflen := len(buf)
if buflen < 1 {
return 0, 0, nil
}
var value = uint64(buf[0]) & 0x7f
n = 1
if (buf[0] & 0x80) != 0 {
if buflen < 2 {
err = fmt.Errorf(truncatedErrStr, 2, buflen)
return
}
value |= uint64(buf[1]) & 0x7f << 7
n = 2
if (buf[1] & 0x80) != 0 {
if buflen < 3 {
err = fmt.Errorf(truncatedErrStr, 3, buflen)
return
}
value |= uint64(buf[2]) & 0x7f << 14
n = 3
if (buf[2] & 0x80) != 0 {
if buflen < 4 {
err = fmt.Errorf(truncatedErrStr, 4, buflen)
return
}
value |= uint64(buf[3]) & 0x7f << 21
n = 4
if (buf[3] & 0x80) != 0 {
if buflen < 5 {
err = fmt.Errorf(truncatedErrStr, 5, buflen)
return
}
value |= uint64(buf[4]) & 0x7f << 28
n = 5
if (buf[4] & 0x80) != 0 {
if buflen < 6 {
err = fmt.Errorf(truncatedErrStr, 6, buflen)
return
}
value |= uint64(buf[5]) & 0x7f << 35
n = 6
if (buf[5] & 0x80) != 0 {
if buflen < 7 {
err = fmt.Errorf(truncatedErrStr, 7, buflen)
return
}
value |= uint64(buf[6]) & 0x7f << 42
n = 7
if (buf[6] & 0x80) != 0 {
if buflen < 8 {
err = fmt.Errorf(truncatedErrStr, 8, buflen)
return
}
value |= uint64(buf[7]) & 0x7f << 49
n = 8
if (buf[7] & 0x80) != 0 {
if buflen < 9 {
err = fmt.Errorf(truncatedErrStr, 9, buflen)
return
}
value |= uint64(buf[8]) << 56
n = 9
}
}
}
}
}
}
}
}
signedValue = int64((value >> 1) ^ -(value & 1))
return
}
// Writes a int64_t value to the given buffer in LEB128 ZigZag encoded format
// ZigZag encoding maps signed integers to unsigned integers so that numbers with a small
// absolute value (for instance, -1) have a small varint encoded value too.
// It does this in a way that "zig-zags" back and forth through the positive and negative integers,
// so that -1 is encoded as 1, 1 is encoded as 2, -2 is encoded as 3, and so on.
func zig_zag_encode_i64(signedValue int64) (buffer []byte) {
buffer = make([]byte, 0)
var value = uint64((signedValue << 1) ^ (signedValue >> 63))
if value>>7 == 0 {
buffer = append(buffer, byte(value))
} else {
buffer = append(buffer, byte((value&0x7F)|0x80))
if value>>14 == 0 {
buffer = append(buffer, byte(value>>7))
} else {
buffer = append(buffer, byte((value>>7)|0x80))
if value>>21 == 0 {
buffer = append(buffer, byte(value>>14))
} else {
buffer = append(buffer, byte((value>>14)|0x80))
if value>>28 == 0 {
buffer = append(buffer, byte(value>>21))
} else {
buffer = append(buffer, byte((value>>21)|0x80))
if value>>35 == 0 {
buffer = append(buffer, byte(value>>28))
} else {
buffer = append(buffer, byte((value>>28)|0x80))
if value>>42 == 0 {
buffer = append(buffer, byte(value>>35))
} else {
buffer = append(buffer, byte((value>>35)|0x80))
if value>>49 == 0 {
buffer = append(buffer, byte(value>>42))
} else {
buffer = append(buffer, byte((value>>42)|0x80))
if value>>56 == 0 {
buffer = append(buffer, byte(value>>49))
} else {
buffer = append(buffer, byte((value>>49)|0x80))
buffer = append(buffer, byte(value>>56))
}
}
}
}
}
}
}
}
return
}

20
vendor/github.com/beorn7/perks/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,20 @@
Copyright (C) 2013 Blake Mizerany
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.

2388
vendor/github.com/beorn7/perks/quantile/exampledata.txt generated vendored Normal file

File diff suppressed because it is too large Load Diff

316
vendor/github.com/beorn7/perks/quantile/stream.go generated vendored Normal file
View File

@@ -0,0 +1,316 @@
// Package quantile computes approximate quantiles over an unbounded data
// stream within low memory and CPU bounds.
//
// A small amount of accuracy is traded to achieve the above properties.
//
// Multiple streams can be merged before calling Query to generate a single set
// of results. This is meaningful when the streams represent the same type of
// data. See Merge and Samples.
//
// For more detailed information about the algorithm used, see:
//
// Effective Computation of Biased Quantiles over Data Streams
//
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
package quantile
import (
"math"
"sort"
)
// Sample holds an observed value and meta information for compression. JSON
// tags have been added for convenience.
type Sample struct {
Value float64 `json:",string"`
Width float64 `json:",string"`
Delta float64 `json:",string"`
}
// Samples represents a slice of samples. It implements sort.Interface.
type Samples []Sample
func (a Samples) Len() int { return len(a) }
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value }
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type invariant func(s *stream, r float64) float64
// NewLowBiased returns an initialized Stream for low-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the lower ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewLowBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * r
}
return newStream(ƒ)
}
// NewHighBiased returns an initialized Stream for high-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the higher ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewHighBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * (s.n - r)
}
return newStream(ƒ)
}
// NewTargeted returns an initialized Stream concerned with a particular set of
// quantile values that are supplied a priori. Knowing these a priori reduces
// space and computation time. The targets map maps the desired quantiles to
// their absolute errors, i.e. the true quantile of a value returned by a query
// is guaranteed to be within (Quantile±Epsilon).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
func NewTargeted(targetMap map[float64]float64) *Stream {
// Convert map to slice to avoid slow iterations on a map.
// ƒ is called on the hot path, so converting the map to a slice
// beforehand results in significant CPU savings.
targets := targetMapToSlice(targetMap)
ƒ := func(s *stream, r float64) float64 {
var m = math.MaxFloat64
var f float64
for _, t := range targets {
if t.quantile*s.n <= r {
f = (2 * t.epsilon * r) / t.quantile
} else {
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile)
}
if f < m {
m = f
}
}
return m
}
return newStream(ƒ)
}
type target struct {
quantile float64
epsilon float64
}
func targetMapToSlice(targetMap map[float64]float64) []target {
targets := make([]target, 0, len(targetMap))
for quantile, epsilon := range targetMap {
t := target{
quantile: quantile,
epsilon: epsilon,
}
targets = append(targets, t)
}
return targets
}
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
// design. Take care when using across multiple goroutines.
type Stream struct {
*stream
b Samples
sorted bool
}
func newStream(ƒ invariant) *Stream {
x := &stream{ƒ: ƒ}
return &Stream{x, make(Samples, 0, 500), true}
}
// Insert inserts v into the stream.
func (s *Stream) Insert(v float64) {
s.insert(Sample{Value: v, Width: 1})
}
func (s *Stream) insert(sample Sample) {
s.b = append(s.b, sample)
s.sorted = false
if len(s.b) == cap(s.b) {
s.flush()
}
}
// Query returns the computed qth percentiles value. If s was created with
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
// will return an unspecified result.
func (s *Stream) Query(q float64) float64 {
if !s.flushed() {
// Fast path when there hasn't been enough data for a flush;
// this also yields better accuracy for small sets of data.
l := len(s.b)
if l == 0 {
return 0
}
i := int(math.Ceil(float64(l) * q))
if i > 0 {
i -= 1
}
s.maybeSort()
return s.b[i].Value
}
s.flush()
return s.stream.query(q)
}
// Merge merges samples into the underlying streams samples. This is handy when
// merging multiple streams from separate threads, database shards, etc.
//
// ATTENTION: This method is broken and does not yield correct results. The
// underlying algorithm is not capable of merging streams correctly.
func (s *Stream) Merge(samples Samples) {
sort.Sort(samples)
s.stream.merge(samples)
}
// Reset reinitializes and clears the list reusing the samples buffer memory.
func (s *Stream) Reset() {
s.stream.reset()
s.b = s.b[:0]
}
// Samples returns stream samples held by s.
func (s *Stream) Samples() Samples {
if !s.flushed() {
return s.b
}
s.flush()
return s.stream.samples()
}
// Count returns the total number of samples observed in the stream
// since initialization.
func (s *Stream) Count() int {
return len(s.b) + s.stream.count()
}
func (s *Stream) flush() {
s.maybeSort()
s.stream.merge(s.b)
s.b = s.b[:0]
}
func (s *Stream) maybeSort() {
if !s.sorted {
s.sorted = true
sort.Sort(s.b)
}
}
func (s *Stream) flushed() bool {
return len(s.stream.l) > 0
}
type stream struct {
n float64
l []Sample
ƒ invariant
}
func (s *stream) reset() {
s.l = s.l[:0]
s.n = 0
}
func (s *stream) insert(v float64) {
s.merge(Samples{{v, 1, 0}})
}
func (s *stream) merge(samples Samples) {
// TODO(beorn7): This tries to merge not only individual samples, but
// whole summaries. The paper doesn't mention merging summaries at
// all. Unittests show that the merging is inaccurate. Find out how to
// do merges properly.
var r float64
i := 0
for _, sample := range samples {
for ; i < len(s.l); i++ {
c := s.l[i]
if c.Value > sample.Value {
// Insert at position i.
s.l = append(s.l, Sample{})
copy(s.l[i+1:], s.l[i:])
s.l[i] = Sample{
sample.Value,
sample.Width,
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
// TODO(beorn7): How to calculate delta correctly?
}
i++
goto inserted
}
r += c.Width
}
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
i++
inserted:
s.n += sample.Width
r += sample.Width
}
s.compress()
}
func (s *stream) count() int {
return int(s.n)
}
func (s *stream) query(q float64) float64 {
t := math.Ceil(q * s.n)
t += math.Ceil(s.ƒ(s, t) / 2)
p := s.l[0]
var r float64
for _, c := range s.l[1:] {
r += p.Width
if r+c.Width+c.Delta > t {
return p.Value
}
p = c
}
return p.Value
}
func (s *stream) compress() {
if len(s.l) < 2 {
return
}
x := s.l[len(s.l)-1]
xi := len(s.l) - 1
r := s.n - 1 - x.Width
for i := len(s.l) - 2; i >= 0; i-- {
c := s.l[i]
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
x.Width += c.Width
s.l[xi] = x
// Remove element at i.
copy(s.l[i:], s.l[i+1:])
s.l = s.l[:len(s.l)-1]
xi -= 1
} else {
x = c
xi = i
}
r -= c.Width
}
}
func (s *stream) samples() Samples {
samples := make(Samples, len(s.l))
copy(samples, s.l)
return samples
}

View File

@@ -3,8 +3,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2)
[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml)
xxhash is a Go implementation of the 64-bit
[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a
xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a
high-quality hashing algorithm that is much faster than anything in the Go
standard library.
@@ -25,8 +24,11 @@ func (*Digest) WriteString(string) (int, error)
func (*Digest) Sum64() uint64
```
This implementation provides a fast pure-Go implementation and an even faster
assembly implementation for amd64.
The package is written with optimized pure Go and also contains even faster
assembly implementations for amd64 and arm64. If desired, the `purego` build tag
opts into using the Go code even on those architectures.
[xxHash]: http://cyan4973.github.io/xxHash/
## Compatibility
@@ -45,19 +47,20 @@ I recommend using the latest release of Go.
Here are some quick benchmarks comparing the pure-Go and assembly
implementations of Sum64.
| input size | purego | asm |
| --- | --- | --- |
| 5 B | 979.66 MB/s | 1291.17 MB/s |
| 100 B | 7475.26 MB/s | 7973.40 MB/s |
| 4 KB | 17573.46 MB/s | 17602.65 MB/s |
| 10 MB | 17131.46 MB/s | 17142.16 MB/s |
| input size | purego | asm |
| ---------- | --------- | --------- |
| 4 B | 1.3 GB/s | 1.2 GB/s |
| 16 B | 2.9 GB/s | 3.5 GB/s |
| 100 B | 6.9 GB/s | 8.1 GB/s |
| 4 KB | 11.7 GB/s | 16.7 GB/s |
| 10 MB | 12.0 GB/s | 17.3 GB/s |
These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using
the following commands under Go 1.11.2:
These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C
CPU using the following commands under Go 1.19.2:
```
$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes'
$ go test -benchtime 10s -bench '/xxhash,direct,bytes'
benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$')
benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
```
## Projects using this package

10
vendor/github.com/cespare/xxhash/v2/testall.sh generated vendored Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -eu -o pipefail
# Small convenience script for running the tests with various combinations of
# arch/tags. This assumes we're running on amd64 and have qemu available.
go test ./...
go test -tags purego ./...
GOARCH=arm64 go test
GOARCH=arm64 go test -tags purego

View File

@@ -16,19 +16,11 @@ const (
prime5 uint64 = 2870177450012600261
)
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where
// possible in the Go code is worth a small (but measurable) performance boost
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for
// convenience in the Go code in a few places where we need to intentionally
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the
// result overflows a uint64).
var (
prime1v = prime1
prime2v = prime2
prime3v = prime3
prime4v = prime4
prime5v = prime5
)
// Store the primes in an array as well.
//
// The consts are used when possible in Go code to avoid MOVs but we need a
// contiguous array of the assembly code.
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
// Digest implements hash.Hash64.
type Digest struct {
@@ -50,10 +42,10 @@ func New() *Digest {
// Reset clears the Digest's state so that it can be reused.
func (d *Digest) Reset() {
d.v1 = prime1v + prime2
d.v1 = primes[0] + prime2
d.v2 = prime2
d.v3 = 0
d.v4 = -prime1v
d.v4 = -primes[0]
d.total = 0
d.n = 0
}
@@ -69,21 +61,23 @@ func (d *Digest) Write(b []byte) (n int, err error) {
n = len(b)
d.total += uint64(n)
memleft := d.mem[d.n&(len(d.mem)-1):]
if d.n+n < 32 {
// This new data doesn't even fill the current block.
copy(d.mem[d.n:], b)
copy(memleft, b)
d.n += n
return
}
if d.n > 0 {
// Finish off the partial block.
copy(d.mem[d.n:], b)
c := copy(memleft, b)
d.v1 = round(d.v1, u64(d.mem[0:8]))
d.v2 = round(d.v2, u64(d.mem[8:16]))
d.v3 = round(d.v3, u64(d.mem[16:24]))
d.v4 = round(d.v4, u64(d.mem[24:32]))
b = b[32-d.n:]
b = b[c:]
d.n = 0
}
@@ -133,21 +127,20 @@ func (d *Digest) Sum64() uint64 {
h += d.total
i, end := 0, d.n
for ; i+8 <= end; i += 8 {
k1 := round(0, u64(d.mem[i:i+8]))
b := d.mem[:d.n&(len(d.mem)-1)]
for ; len(b) >= 8; b = b[8:] {
k1 := round(0, u64(b[:8]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if i+4 <= end {
h ^= uint64(u32(d.mem[i:i+4])) * prime1
if len(b) >= 4 {
h ^= uint64(u32(b[:4])) * prime1
h = rol23(h)*prime2 + prime3
i += 4
b = b[4:]
}
for i < end {
h ^= uint64(d.mem[i]) * prime5
for ; len(b) > 0; b = b[1:] {
h ^= uint64(b[0]) * prime5
h = rol11(h) * prime1
i++
}
h ^= h >> 33

View File

@@ -1,215 +1,209 @@
//go:build !appengine && gc && !purego
// +build !appengine
// +build gc
// +build !purego
#include "textflag.h"
// Register allocation:
// AX h
// SI pointer to advance through b
// DX n
// BX loop end
// R8 v1, k1
// R9 v2
// R10 v3
// R11 v4
// R12 tmp
// R13 prime1v
// R14 prime2v
// DI prime4v
// Registers:
#define h AX
#define d AX
#define p SI // pointer to advance through b
#define n DX
#define end BX // loop end
#define v1 R8
#define v2 R9
#define v3 R10
#define v4 R11
#define x R12
#define prime1 R13
#define prime2 R14
#define prime4 DI
// round reads from and advances the buffer pointer in SI.
// It assumes that R13 has prime1v and R14 has prime2v.
#define round(r) \
MOVQ (SI), R12 \
ADDQ $8, SI \
IMULQ R14, R12 \
ADDQ R12, r \
ROLQ $31, r \
IMULQ R13, r
#define round(acc, x) \
IMULQ prime2, x \
ADDQ x, acc \
ROLQ $31, acc \
IMULQ prime1, acc
// mergeRound applies a merge round on the two registers acc and val.
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v.
#define mergeRound(acc, val) \
IMULQ R14, val \
ROLQ $31, val \
IMULQ R13, val \
XORQ val, acc \
IMULQ R13, acc \
ADDQ DI, acc
// round0 performs the operation x = round(0, x).
#define round0(x) \
IMULQ prime2, x \
ROLQ $31, x \
IMULQ prime1, x
// mergeRound applies a merge round on the two registers acc and x.
// It assumes that prime1, prime2, and prime4 have been loaded.
#define mergeRound(acc, x) \
round0(x) \
XORQ x, acc \
IMULQ prime1, acc \
ADDQ prime4, acc
// blockLoop processes as many 32-byte blocks as possible,
// updating v1, v2, v3, and v4. It assumes that there is at least one block
// to process.
#define blockLoop() \
loop: \
MOVQ +0(p), x \
round(v1, x) \
MOVQ +8(p), x \
round(v2, x) \
MOVQ +16(p), x \
round(v3, x) \
MOVQ +24(p), x \
round(v4, x) \
ADDQ $32, p \
CMPQ p, end \
JLE loop
// func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT, $0-32
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
// Load fixed primes.
MOVQ ·prime1v(SB), R13
MOVQ ·prime2v(SB), R14
MOVQ ·prime4v(SB), DI
MOVQ ·primes+0(SB), prime1
MOVQ ·primes+8(SB), prime2
MOVQ ·primes+24(SB), prime4
// Load slice.
MOVQ b_base+0(FP), SI
MOVQ b_len+8(FP), DX
LEAQ (SI)(DX*1), BX
MOVQ b_base+0(FP), p
MOVQ b_len+8(FP), n
LEAQ (p)(n*1), end
// The first loop limit will be len(b)-32.
SUBQ $32, BX
SUBQ $32, end
// Check whether we have at least one block.
CMPQ DX, $32
CMPQ n, $32
JLT noBlocks
// Set up initial state (v1, v2, v3, v4).
MOVQ R13, R8
ADDQ R14, R8
MOVQ R14, R9
XORQ R10, R10
XORQ R11, R11
SUBQ R13, R11
MOVQ prime1, v1
ADDQ prime2, v1
MOVQ prime2, v2
XORQ v3, v3
XORQ v4, v4
SUBQ prime1, v4
// Loop until SI > BX.
blockLoop:
round(R8)
round(R9)
round(R10)
round(R11)
blockLoop()
CMPQ SI, BX
JLE blockLoop
MOVQ v1, h
ROLQ $1, h
MOVQ v2, x
ROLQ $7, x
ADDQ x, h
MOVQ v3, x
ROLQ $12, x
ADDQ x, h
MOVQ v4, x
ROLQ $18, x
ADDQ x, h
MOVQ R8, AX
ROLQ $1, AX
MOVQ R9, R12
ROLQ $7, R12
ADDQ R12, AX
MOVQ R10, R12
ROLQ $12, R12
ADDQ R12, AX
MOVQ R11, R12
ROLQ $18, R12
ADDQ R12, AX
mergeRound(AX, R8)
mergeRound(AX, R9)
mergeRound(AX, R10)
mergeRound(AX, R11)
mergeRound(h, v1)
mergeRound(h, v2)
mergeRound(h, v3)
mergeRound(h, v4)
JMP afterBlocks
noBlocks:
MOVQ ·prime5v(SB), AX
MOVQ ·primes+32(SB), h
afterBlocks:
ADDQ DX, AX
ADDQ n, h
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8.
ADDQ $24, BX
ADDQ $24, end
CMPQ p, end
JG try4
CMPQ SI, BX
JG fourByte
loop8:
MOVQ (p), x
ADDQ $8, p
round0(x)
XORQ x, h
ROLQ $27, h
IMULQ prime1, h
ADDQ prime4, h
wordLoop:
// Calculate k1.
MOVQ (SI), R8
ADDQ $8, SI
IMULQ R14, R8
ROLQ $31, R8
IMULQ R13, R8
CMPQ p, end
JLE loop8
XORQ R8, AX
ROLQ $27, AX
IMULQ R13, AX
ADDQ DI, AX
try4:
ADDQ $4, end
CMPQ p, end
JG try1
CMPQ SI, BX
JLE wordLoop
MOVL (p), x
ADDQ $4, p
IMULQ prime1, x
XORQ x, h
fourByte:
ADDQ $4, BX
CMPQ SI, BX
JG singles
ROLQ $23, h
IMULQ prime2, h
ADDQ ·primes+16(SB), h
MOVL (SI), R8
ADDQ $4, SI
IMULQ R13, R8
XORQ R8, AX
ROLQ $23, AX
IMULQ R14, AX
ADDQ ·prime3v(SB), AX
singles:
ADDQ $4, BX
CMPQ SI, BX
try1:
ADDQ $4, end
CMPQ p, end
JGE finalize
singlesLoop:
MOVBQZX (SI), R12
ADDQ $1, SI
IMULQ ·prime5v(SB), R12
XORQ R12, AX
loop1:
MOVBQZX (p), x
ADDQ $1, p
IMULQ ·primes+32(SB), x
XORQ x, h
ROLQ $11, h
IMULQ prime1, h
ROLQ $11, AX
IMULQ R13, AX
CMPQ SI, BX
JL singlesLoop
CMPQ p, end
JL loop1
finalize:
MOVQ AX, R12
SHRQ $33, R12
XORQ R12, AX
IMULQ R14, AX
MOVQ AX, R12
SHRQ $29, R12
XORQ R12, AX
IMULQ ·prime3v(SB), AX
MOVQ AX, R12
SHRQ $32, R12
XORQ R12, AX
MOVQ h, x
SHRQ $33, x
XORQ x, h
IMULQ prime2, h
MOVQ h, x
SHRQ $29, x
XORQ x, h
IMULQ ·primes+16(SB), h
MOVQ h, x
SHRQ $32, x
XORQ x, h
MOVQ AX, ret+24(FP)
MOVQ h, ret+24(FP)
RET
// writeBlocks uses the same registers as above except that it uses AX to store
// the d pointer.
// func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT, $0-40
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
// Load fixed primes needed for round.
MOVQ ·prime1v(SB), R13
MOVQ ·prime2v(SB), R14
MOVQ ·primes+0(SB), prime1
MOVQ ·primes+8(SB), prime2
// Load slice.
MOVQ b_base+8(FP), SI
MOVQ b_len+16(FP), DX
LEAQ (SI)(DX*1), BX
SUBQ $32, BX
MOVQ b_base+8(FP), p
MOVQ b_len+16(FP), n
LEAQ (p)(n*1), end
SUBQ $32, end
// Load vN from d.
MOVQ d+0(FP), AX
MOVQ 0(AX), R8 // v1
MOVQ 8(AX), R9 // v2
MOVQ 16(AX), R10 // v3
MOVQ 24(AX), R11 // v4
MOVQ s+0(FP), d
MOVQ 0(d), v1
MOVQ 8(d), v2
MOVQ 16(d), v3
MOVQ 24(d), v4
// We don't need to check the loop condition here; this function is
// always called with at least one block of data to process.
blockLoop:
round(R8)
round(R9)
round(R10)
round(R11)
CMPQ SI, BX
JLE blockLoop
blockLoop()
// Copy vN back to d.
MOVQ R8, 0(AX)
MOVQ R9, 8(AX)
MOVQ R10, 16(AX)
MOVQ R11, 24(AX)
MOVQ v1, 0(d)
MOVQ v2, 8(d)
MOVQ v3, 16(d)
MOVQ v4, 24(d)
// The number of bytes written is SI minus the old base pointer.
SUBQ b_base+8(FP), SI
MOVQ SI, ret+32(FP)
// The number of bytes written is p minus the old base pointer.
SUBQ b_base+8(FP), p
MOVQ p, ret+32(FP)
RET

183
vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s generated vendored Normal file
View File

@@ -0,0 +1,183 @@
//go:build !appengine && gc && !purego
// +build !appengine
// +build gc
// +build !purego
#include "textflag.h"
// Registers:
#define digest R1
#define h R2 // return value
#define p R3 // input pointer
#define n R4 // input length
#define nblocks R5 // n / 32
#define prime1 R7
#define prime2 R8
#define prime3 R9
#define prime4 R10
#define prime5 R11
#define v1 R12
#define v2 R13
#define v3 R14
#define v4 R15
#define x1 R20
#define x2 R21
#define x3 R22
#define x4 R23
#define round(acc, x) \
MADD prime2, acc, x, acc \
ROR $64-31, acc \
MUL prime1, acc
// round0 performs the operation x = round(0, x).
#define round0(x) \
MUL prime2, x \
ROR $64-31, x \
MUL prime1, x
#define mergeRound(acc, x) \
round0(x) \
EOR x, acc \
MADD acc, prime4, prime1, acc
// blockLoop processes as many 32-byte blocks as possible,
// updating v1, v2, v3, and v4. It assumes that n >= 32.
#define blockLoop() \
LSR $5, n, nblocks \
PCALIGN $16 \
loop: \
LDP.P 16(p), (x1, x2) \
LDP.P 16(p), (x3, x4) \
round(v1, x1) \
round(v2, x2) \
round(v3, x3) \
round(v4, x4) \
SUB $1, nblocks \
CBNZ nblocks, loop
// func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
LDP b_base+0(FP), (p, n)
LDP ·primes+0(SB), (prime1, prime2)
LDP ·primes+16(SB), (prime3, prime4)
MOVD ·primes+32(SB), prime5
CMP $32, n
CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 }
BLT afterLoop
ADD prime1, prime2, v1
MOVD prime2, v2
MOVD $0, v3
NEG prime1, v4
blockLoop()
ROR $64-1, v1, x1
ROR $64-7, v2, x2
ADD x1, x2
ROR $64-12, v3, x3
ROR $64-18, v4, x4
ADD x3, x4
ADD x2, x4, h
mergeRound(h, v1)
mergeRound(h, v2)
mergeRound(h, v3)
mergeRound(h, v4)
afterLoop:
ADD n, h
TBZ $4, n, try8
LDP.P 16(p), (x1, x2)
round0(x1)
// NOTE: here and below, sequencing the EOR after the ROR (using a
// rotated register) is worth a small but measurable speedup for small
// inputs.
ROR $64-27, h
EOR x1 @> 64-27, h, h
MADD h, prime4, prime1, h
round0(x2)
ROR $64-27, h
EOR x2 @> 64-27, h, h
MADD h, prime4, prime1, h
try8:
TBZ $3, n, try4
MOVD.P 8(p), x1
round0(x1)
ROR $64-27, h
EOR x1 @> 64-27, h, h
MADD h, prime4, prime1, h
try4:
TBZ $2, n, try2
MOVWU.P 4(p), x2
MUL prime1, x2
ROR $64-23, h
EOR x2 @> 64-23, h, h
MADD h, prime3, prime2, h
try2:
TBZ $1, n, try1
MOVHU.P 2(p), x3
AND $255, x3, x1
LSR $8, x3, x2
MUL prime5, x1
ROR $64-11, h
EOR x1 @> 64-11, h, h
MUL prime1, h
MUL prime5, x2
ROR $64-11, h
EOR x2 @> 64-11, h, h
MUL prime1, h
try1:
TBZ $0, n, finalize
MOVBU (p), x4
MUL prime5, x4
ROR $64-11, h
EOR x4 @> 64-11, h, h
MUL prime1, h
finalize:
EOR h >> 33, h
MUL prime2, h
EOR h >> 29, h
MUL prime3, h
EOR h >> 32, h
MOVD h, ret+24(FP)
RET
// func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
LDP ·primes+0(SB), (prime1, prime2)
// Load state. Assume v[1-4] are stored contiguously.
MOVD d+0(FP), digest
LDP 0(digest), (v1, v2)
LDP 16(digest), (v3, v4)
LDP b_base+8(FP), (p, n)
blockLoop()
// Store updated state.
STP (v1, v2), 0(digest)
STP (v3, v4), 16(digest)
BIC $31, n
MOVD n, ret+32(FP)
RET

View File

@@ -1,3 +1,5 @@
//go:build (amd64 || arm64) && !appengine && gc && !purego
// +build amd64 arm64
// +build !appengine
// +build gc
// +build !purego

View File

@@ -1,4 +1,5 @@
// +build !amd64 appengine !gc purego
//go:build (!amd64 && !arm64) || appengine || !gc || purego
// +build !amd64,!arm64 appengine !gc purego
package xxhash
@@ -14,10 +15,10 @@ func Sum64(b []byte) uint64 {
var h uint64
if n >= 32 {
v1 := prime1v + prime2
v1 := primes[0] + prime2
v2 := prime2
v3 := uint64(0)
v4 := -prime1v
v4 := -primes[0]
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
@@ -36,19 +37,18 @@ func Sum64(b []byte) uint64 {
h += uint64(n)
i, end := 0, len(b)
for ; i+8 <= end; i += 8 {
k1 := round(0, u64(b[i:i+8:len(b)]))
for ; len(b) >= 8; b = b[8:] {
k1 := round(0, u64(b[:8]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if i+4 <= end {
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1
if len(b) >= 4 {
h ^= uint64(u32(b[:4])) * prime1
h = rol23(h)*prime2 + prime3
i += 4
b = b[4:]
}
for ; i < end; i++ {
h ^= uint64(b[i]) * prime5
for ; len(b) > 0; b = b[1:] {
h ^= uint64(b[0]) * prime5
h = rol11(h) * prime1
}

View File

@@ -1,3 +1,4 @@
//go:build appengine
// +build appengine
// This file contains the safe implementations of otherwise unsafe-using code.

View File

@@ -1,3 +1,4 @@
//go:build !appengine
// +build !appengine
// This file encapsulates usage of unsafe.
@@ -11,7 +12,7 @@ import (
// In the future it's possible that compiler optimizations will make these
// XxxString functions unnecessary by realizing that calls such as
// Sum64([]byte(s)) don't need to copy s. See https://golang.org/issue/2205.
// Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205.
// If that happens, even if we keep these functions they can be replaced with
// the trivial safe code.

View File

@@ -1,6 +0,0 @@
language: go
go:
- 1.13.x
- 1.14.x
- 1.15.x

View File

@@ -6,13 +6,16 @@
# go get github.com/gogo/protobuf/types
# go get github.com/gogo/protobuf/protoc-gen-gogo
#
# Note: as of 2020-11-01, it is not (yet) possible to use gogoproto
# v1.3.x to generate .pb.go files so that they are compatible with
# CockroachDB. This is because CockroachDB hardcodes gogoproto at
# version v1.2, and thus does not provide v1.3's
# proto.GoGoProtoPackageIsVersion3.
# Note: as of 2021-04-13, we like to use a custom protoc-gen-gogo
# with additional options, to stabilize the marshalled
# encoding of objects (so that they are deterministic
# across marshal/unmarshal cycles) and reduce the memory footprint
# of objects:
#
# For details, see https://github.com/cockroachdb/cockroach/issues/56378
# vanity.TurnOnStable_MarshalerAll,
# vanity.TurnOffGoUnrecognizedAll,
# vanity.TurnOffGoUnkeyedAll,
# vanity.TurnOffGoSizecacheAll,
#
# Until this is resolved, the "go get" commands above are not
# adequate; instead:
@@ -23,21 +26,33 @@
#
# 2. run `make -f Makefile.update-protos` with this PATH active.
PROTOS := $(wildcard errorspb/*.proto)
export SHELL := env PWD=$(CURDIR) bash
PROTOS := $(wildcard \
errbase/internal/*.proto \
errorspb/*.proto \
extgrpc/*.proto \
exthttp/*.proto \
grpc/*.proto \
markers/internal/*.proto \
)
GO_SOURCES = $(PROTOS:.proto=.pb.go)
SED = sed
SED_INPLACE := $(shell $(SED) --version 2>&1 | grep -q GNU && echo -i || echo "-i ''")
all: $(PROTOS)
protoc \
-I$$GOPATH/src/ \
-I$$GOPATH/src/github.com \
-I$$GOPATH/src/github.com/gogo/protobuf \
-I$$GOPATH/src/github.com/gogo/protobuf/protobuf \
--gogoroach_out=Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types,plugins=grpc,import_prefix=:. \
$(PROTOS:%=$(GOPATH)/src/github.com/cockroachdb/errors/%)
mv -f github.com/cockroachdb/errors/errorspb/*.pb.go errorspb/
set -e; for dir in $(sort $(dir $(PROTOS))); do \
protoc \
-I. \
-I$$GOPATH/src/ \
-I$$GOPATH/src/github.com \
-I$$GOPATH/src/github.com/cockroachdb/errors \
-I$$GOPATH/src/github.com/gogo/protobuf \
-I$$GOPATH/src/github.com/gogo/protobuf/protobuf \
--gogoroach_out=Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types,plugins=grpc,import_prefix=:. \
$$dir/*.proto; \
done
$(SED) $(SED_INPLACE) -E \
-e '/import _ /d' \
-e 's!import (fmt|math) "github.com/(fmt|math)"! !g' \

View File

@@ -11,7 +11,8 @@ automatically formats error details and strips them of PII.
See also [the design RFC](https://github.com/cockroachdb/cockroach/blob/master/docs/RFCS/20190318_error_handling.md).
[![Build Status](https://travis-ci.org/cockroachdb/errors.svg?branch=master)](https://travis-ci.org/cockroachdb/errors)
![Build Status](https://github.com/cockroachdb/errors/actions/workflows/ci.yaml/badge.svg?branch=master)
[![Go Reference](https://pkg.go.dev/badge/github.com/cockroachdb/errors.svg)](https://pkg.go.dev/github.com/cockroachdb/errors)
Table of contents:
@@ -86,7 +87,7 @@ older version of the package.
| detail, eg `WithDetail()` | not visible | visible | no | type only |
| assertion failure annotation, eg `WithAssertionFailure()` | not visible | visible | no | type only |
| issue links, eg `WithIssueLink()`, `UnimplementedError()` | not visible | visible | yes | full |
| safe details, eg `WithSafeDetails()` | not visible | not visible | yes | full |
| safe details, eg `WithSafeDetails()` | not visible | visible | yes | full |
| telemetry keys, eg. `WithTelemetryKey()` | not visible | visible | yes | full |
| secondary errors, eg. `WithSecondaryError()`, `CombineErrors()` | not visible | visible | redacted, recursively | redacted, recursively |
| barrier origins, eg. `Handled()` | not visible | visible | redacted, recursively | redacted, recursively |
@@ -208,12 +209,12 @@ return errors.Wrap(foo(), "foo")
- how to access the detail: `Error()`, regular Go formatting, Sentry Report.
- `WithDetail(error, string) error`, `WithDetailf(error, string, ...interface{}) error`, user-facing detail with contextual information.
- **when to use: need to embark a message string to output when the error is presented to a human.**
- **when to use: need to embark a message string to output when the error is presented to a developer.**
- what it does: captures detail strings.
- how to access the detail: `errors.GetAllDetails()`, `errors.FlattenDetails()` (all details are preserved), format with `%+v`. Not included in Sentry reports.
- `WithHint(error, string) error`, `WithHintf(error, string, ...interface{}) error`: user-facing detail with suggestion for action to take.
- **when to use: need to embark a message string to output when the error is presented to a human.**
- **when to use: need to embark a message string to output when the error is presented to an end user.**
- what it does: captures hint strings.
- how to access the detail: `errors.GetAllHints()`, `errors.FlattenHints()` (hints are de-duplicated), format with `%+v`. Not included in Sentry reports.
@@ -422,10 +423,10 @@ func myLeafDecoder(_ string, details []string, _ proto.Message) error {
(For an example, see the `withTelemetry` type in [`telemetry/with_telemetry.go`](telemetry/with_telemetry.go).)
**The only case where you need a custom encoder is when your error
__The only case where you need a custom encoder is when your error
type contains some fields that are not reflected in the error message
(so you can't extract them back from there), and are not PII-free and
thus cannot be reported as "safe details".**
thus cannot be reported as "safe details".__
To take inspiration from examples, see the following types in the
library that need a custom encoder:
@@ -535,9 +536,13 @@ Example use:
| `WrapWithDepthf` | `WithMessagef` + `WithStackDepth` |
| `AssertionFailedWithDepthf` | `NewWithDepthf` + `WithAssertionFailure` |
| `NewAssertionErrorWithWrappedErrf` | `HandledWithMessagef` (barrier) + `WrapWithDepthf` + `WithAssertionFailure` |
| `Join` | `JoinWithDepth` (see below) |
| `JoinWithDepth` | multi-cause wrapper + `WithStackDepth` |
## API (not constructing error objects)
The following is a summary of the non-constructor API functions, grouped by category.
Detailed documentation can be found at: https://pkg.go.dev/github.com/cockroachdb/errors
```go
// Access causes.
func UnwrapAll(err error) error
@@ -569,10 +574,16 @@ func RegisterLeafDecoder(typeName TypeKey, decoder LeafDecoder)
func RegisterLeafEncoder(typeName TypeKey, encoder LeafEncoder)
func RegisterWrapperDecoder(typeName TypeKey, decoder WrapperDecoder)
func RegisterWrapperEncoder(typeName TypeKey, encoder WrapperEncoder)
func RegisterWrapperEncoderWithMessageOverride (typeName TypeKey, encoder WrapperEncoderWithMessageOverride)
func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder)
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder)
type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
type LeafDecoder = func(ctx context.Context, msg string, safeDetails []string, payload proto.Message) error
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message)
type WrapperEncoderWithMessageOverride = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, overrideError bool)
type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, safeDetails []string, payload proto.Message) error
type MultiCauseEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
type MultiCauseDecoder = func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error
// Registering package renames for custom error types.
func RegisterTypeMigration(previousPkgPath, previousTypeName string, newType error)

View File

@@ -19,6 +19,7 @@ import (
"fmt"
"github.com/cockroachdb/errors/errbase"
"github.com/cockroachdb/redact"
"github.com/gogo/protobuf/proto"
)
@@ -36,7 +37,7 @@ func Handled(err error) error {
if err == nil {
return nil
}
return HandledWithMessage(err, err.Error())
return HandledWithSafeMessage(err, redact.Sprint(err))
}
// HandledWithMessage is like Handled except the message is overridden.
@@ -46,7 +47,14 @@ func HandledWithMessage(err error, msg string) error {
if err == nil {
return nil
}
return &barrierError{maskedErr: err, msg: msg}
return HandledWithSafeMessage(err, redact.Sprint(msg))
}
// HandledWithSafeMessage is like Handled except the message is overridden.
// This can be used e.g. to hide message details or to prevent
// downstream code to make assertions on the message's contents.
func HandledWithSafeMessage(err error, msg redact.RedactableString) error {
return &barrierErr{maskedErr: err, smsg: msg}
}
// HandledWithMessagef is like HandledWithMessagef except the message
@@ -55,47 +63,48 @@ func HandledWithMessagef(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return &barrierError{maskedErr: err, msg: fmt.Sprintf(format, args...)}
return &barrierErr{maskedErr: err, smsg: redact.Sprintf(format, args...)}
}
// barrierError is a leaf error type. It encapsulates a chain of
// barrierErr is a leaf error type. It encapsulates a chain of
// original causes, but these causes are hidden so that they inhibit
// matching via Is() and the Cause()/Unwrap() recursions.
type barrierError struct {
type barrierErr struct {
// Message for the barrier itself.
// In the common case, the message from the masked error
// is used as-is (see Handled() above) however it is
// useful to cache it here since the masked error may
// have a long chain of wrappers and its Error() call
// may be expensive.
msg string
smsg redact.RedactableString
// Masked error chain.
maskedErr error
}
var _ error = (*barrierError)(nil)
var _ errbase.SafeDetailer = (*barrierError)(nil)
var _ errbase.SafeFormatter = (*barrierError)(nil)
var _ fmt.Formatter = (*barrierError)(nil)
var _ error = (*barrierErr)(nil)
var _ errbase.SafeDetailer = (*barrierErr)(nil)
var _ errbase.SafeFormatter = (*barrierErr)(nil)
var _ fmt.Formatter = (*barrierErr)(nil)
// barrierError is an error.
func (e *barrierError) Error() string { return e.msg }
// barrierErr is an error.
func (e *barrierErr) Error() string { return e.smsg.StripMarkers() }
// SafeDetails reports the PII-free details from the masked error.
func (e *barrierError) SafeDetails() []string {
func (e *barrierErr) SafeDetails() []string {
var details []string
for err := e.maskedErr; err != nil; err = errbase.UnwrapOnce(err) {
sd := errbase.GetSafeDetails(err)
details = sd.Fill(details)
}
details = append(details, redact.Sprintf("masked error: %+v", e.maskedErr).Redact().StripMarkers())
return details
}
// Printing a barrier reveals the details.
func (e *barrierError) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) }
func (e *barrierErr) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) }
func (e *barrierError) SafeFormatError(p errbase.Printer) (next error) {
p.Print(e.msg)
func (e *barrierErr) SafeFormatError(p errbase.Printer) (next error) {
p.Print(e.smsg)
if p.Detail() {
p.Printf("-- cause hidden behind barrier\n%+v", e.maskedErr)
}
@@ -106,19 +115,37 @@ func (e *barrierError) SafeFormatError(p errbase.Printer) (next error) {
func encodeBarrier(
ctx context.Context, err error,
) (msg string, details []string, payload proto.Message) {
e := err.(*barrierError)
e := err.(*barrierErr)
enc := errbase.EncodeError(ctx, e.maskedErr)
return e.msg, e.SafeDetails(), &enc
return string(e.smsg), e.SafeDetails(), &enc
}
// A barrier error is decoded exactly.
func decodeBarrier(ctx context.Context, msg string, _ []string, payload proto.Message) error {
enc := payload.(*errbase.EncodedError)
return &barrierError{msg: msg, maskedErr: errbase.DecodeError(ctx, *enc)}
return &barrierErr{smsg: redact.RedactableString(msg), maskedErr: errbase.DecodeError(ctx, *enc)}
}
// Previous versions of barrier errors.
func decodeBarrierPrev(ctx context.Context, msg string, _ []string, payload proto.Message) error {
enc := payload.(*errbase.EncodedError)
return &barrierErr{smsg: redact.Sprint(msg), maskedErr: errbase.DecodeError(ctx, *enc)}
}
// barrierError is the "old" type name of barrierErr. We use a new
// name now to ensure a different decode function is used when
// importing barriers from the previous structure, where the
// message is not redactable.
type barrierError struct {
msg string
maskedErr error
}
func (b *barrierError) Error() string { return "" }
func init() {
tn := errbase.GetTypeKey((*barrierError)(nil))
errbase.RegisterLeafDecoder(errbase.GetTypeKey((*barrierError)(nil)), decodeBarrierPrev)
tn := errbase.GetTypeKey((*barrierErr)(nil))
errbase.RegisterLeafDecoder(tn, decodeBarrier)
errbase.RegisterLeafEncoder(tn, encodeBarrier)
}

View File

@@ -25,7 +25,7 @@ import (
// WithContextTags captures the k/v pairs stored in the context via the
// `logtags` package and annotates them on the error.
//
// Only the stromg representation of values remains available. This is
// Only the strong representation of values remains available. This is
// because the library cannot guarantee that the underlying value is
// preserved across the network. To avoid creating a stateful interface
// (where the user code needs to know whether an error has traveled

View File

@@ -24,7 +24,7 @@ import (
// WithContextTags captures the k/v pairs stored in the context via the
// `logtags` package and annotates them on the error.
//
// Only the stromg representation of values remains available. This is
// Only the strong representation of values remains available. This is
// because the library cannot guarantee that the underlying value is
// preserved across the network. To avoid creating a stateful interface
// (where the user code needs to know whether an error has traveled

View File

@@ -37,6 +37,11 @@ func decodeErrorString(_ context.Context, msg string, _ []string, _ proto.Messag
return goErr.New(msg)
}
// context.DeadlineExceeded uses a custom type.
func decodeDeadlineExceeded(_ context.Context, _ string, _ []string, _ proto.Message) error {
return context.DeadlineExceeded
}
// errors.fundamental from github.com/pkg/errors cannot be encoded
// exactly because it includes a non-serializable stack trace
// object. In order to work with it, we encode it by dumping
@@ -183,6 +188,8 @@ func init() {
baseErr := goErr.New("")
RegisterLeafDecoder(GetTypeKey(baseErr), decodeErrorString)
RegisterLeafDecoder(GetTypeKey(context.DeadlineExceeded), decodeDeadlineExceeded)
pkgE := pkgErr.New("")
RegisterLeafEncoder(GetTypeKey(pkgE), encodePkgFundamental)
@@ -191,9 +198,11 @@ func init() {
ws := pkgErr.WithStack(baseErr)
RegisterWrapperEncoder(GetTypeKey(ws), encodePkgWithStack)
registerOsPathErrorMigration() // Needed for Go 1.16.
pKey := GetTypeKey(&os.PathError{})
RegisterWrapperEncoder(pKey, encodePathError)
RegisterWrapperDecoder(pKey, decodePathError)
pKey = GetTypeKey(&os.LinkError{})
RegisterWrapperEncoder(pKey, encodeLinkError)
RegisterWrapperDecoder(pKey, decodeLinkError)

View File

@@ -23,6 +23,8 @@ import (
)
// DecodeError decodes an error.
//
// Can only be called if the EncodedError is set (see IsSet()).
func DecodeError(ctx context.Context, enc EncodedError) error {
if w := enc.GetWrapper(); w != nil {
return decodeWrapper(ctx, w)
@@ -55,6 +57,15 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
return genErr
}
// Decoding failed, we'll drop through to opaqueLeaf{} below.
} else if decoder, ok := multiCauseDecoders[typeKey]; ok {
causes := make([]error, len(enc.MultierrorCauses))
for i, e := range enc.MultierrorCauses {
causes[i] = DecodeError(ctx, *e)
}
genErr := decoder(ctx, causes, enc.Message, enc.Details.ReportablePayload, payload)
if genErr != nil {
return genErr
}
} else {
// Shortcut for non-registered proto-encodable error types:
// if it already implements `error`, it's good to go.
@@ -64,6 +75,19 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
}
}
if len(enc.MultierrorCauses) > 0 {
causes := make([]error, len(enc.MultierrorCauses))
for i, e := range enc.MultierrorCauses {
causes[i] = DecodeError(ctx, *e)
}
leaf := &opaqueLeafCauses{
causes: causes,
}
leaf.msg = enc.Message
leaf.details = enc.Details
return leaf
}
// No decoder and no error type: we'll keep what we received and
// make it ready to re-encode exactly (if the error leaves over the
// network again).
@@ -95,7 +119,7 @@ func decodeWrapper(ctx context.Context, enc *errorspb.EncodedWrapper) error {
typeKey := TypeKey(enc.Details.ErrorTypeMark.FamilyName)
if decoder, ok := decoders[typeKey]; ok {
// Yes, use it.
genErr := decoder(ctx, cause, enc.MessagePrefix, enc.Details.ReportablePayload, payload)
genErr := decoder(ctx, cause, enc.Message, enc.Details.ReportablePayload, payload)
if genErr != nil {
// Decoding succeeded. Use this.
return genErr
@@ -105,9 +129,10 @@ func decodeWrapper(ctx context.Context, enc *errorspb.EncodedWrapper) error {
// Otherwise, preserve all details about the original object.
return &opaqueWrapper{
cause: cause,
prefix: enc.MessagePrefix,
details: enc.Details,
cause: cause,
prefix: enc.Message,
details: enc.Details,
messageType: MessageType(enc.MessageType),
}
}
@@ -158,3 +183,24 @@ type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, s
// registry for RegisterWrapperType.
var decoders = map[TypeKey]WrapperDecoder{}
// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
// above) by additional multi-cause wrapper types not yet known by the
// library. A nil return indicates that decoding was not successful.
type MultiCauseDecoder = func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error
// registry for RegisterMultiCauseDecoder.
var multiCauseDecoders = map[TypeKey]MultiCauseDecoder{}
// RegisterMultiCauseDecoder can be used to register new multi-cause
// wrapper types to the library. Registered wrappers will be decoded
// using their own Go type when an error is decoded. Multi-cause
// wrappers that have not been registered will be decoded using the
// opaqueWrapper type.
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
if decoder == nil {
delete(multiCauseDecoders, theType)
} else {
multiCauseDecoders[theType] = decoder
}
}

View File

@@ -33,18 +33,24 @@ func EncodeError(ctx context.Context, err error) EncodedError {
if cause := UnwrapOnce(err); cause != nil {
return encodeWrapper(ctx, err, cause)
}
// Not a causer.
return encodeLeaf(ctx, err)
return encodeLeaf(ctx, err, UnwrapMulti(err))
}
// encodeLeaf encodes a leaf error.
func encodeLeaf(ctx context.Context, err error) EncodedError {
// encodeLeaf encodes a leaf error. This function accepts a `causes`
// argument because we encode multi-cause errors using the Leaf
// protobuf. This was done to enable backwards compatibility when
// introducing this functionality since the Wrapper type already has a
// required single `cause` field.
func encodeLeaf(ctx context.Context, err error, causes []error) EncodedError {
var msg string
var details errorspb.EncodedErrorDetails
if e, ok := err.(*opaqueLeaf); ok {
msg = e.msg
details = e.details
} else if e, ok := err.(*opaqueLeafCauses); ok {
msg = e.msg
details = e.details
} else {
details.OriginalTypeName, details.ErrorTypeMark.FamilyName, details.ErrorTypeMark.Extension = getTypeDetails(err, false /*onlyFamily*/)
@@ -74,11 +80,21 @@ func encodeLeaf(ctx context.Context, err error) EncodedError {
details.FullDetails = encodeAsAny(ctx, err, payload)
}
var cs []*EncodedError
if len(causes) > 0 {
cs = make([]*EncodedError, len(causes))
for i, ee := range causes {
ee := EncodeError(ctx, ee)
cs[i] = &ee
}
}
return EncodedError{
Error: &errorspb.EncodedError_Leaf{
Leaf: &errorspb.EncodedErrorLeaf{
Message: msg,
Details: details,
Message: msg,
Details: details,
MultierrorCauses: cs,
},
},
}
@@ -115,10 +131,18 @@ func encodeAsAny(ctx context.Context, err error, payload proto.Message) *types.A
func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
var msg string
var details errorspb.EncodedErrorDetails
messageType := Prefix
if e, ok := err.(*opaqueWrapper); ok {
// We delegate all knowledge of the error string
// to the original encoder and do not try to re-engineer
// the prefix out of the error. This helps maintain
// backward compatibility with earlier versions of the
// encoder which don't have any understanding of
// error string ownership by the wrapper.
msg = e.prefix
details = e.details
messageType = e.messageType
} else {
details.OriginalTypeName, details.ErrorTypeMark.FamilyName, details.ErrorTypeMark.Extension = getTypeDetails(err, false /*onlyFamily*/)
@@ -127,12 +151,12 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
// If we have a manually registered encoder, use that.
typeKey := TypeKey(details.ErrorTypeMark.FamilyName)
if enc, ok := encoders[typeKey]; ok {
msg, details.ReportablePayload, payload = enc(ctx, err)
msg, details.ReportablePayload, payload, messageType = enc(ctx, err)
} else {
// No encoder.
// In that case, we'll try to compute a message prefix
// manually.
msg = extractPrefix(err, cause)
msg, messageType = extractPrefix(err, cause)
// If there are known safe details, use them.
if s, ok := err.(SafeDetailer); ok {
@@ -148,9 +172,10 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
return EncodedError{
Error: &errorspb.EncodedError_Wrapper{
Wrapper: &errorspb.EncodedWrapper{
Cause: EncodeError(ctx, cause),
MessagePrefix: msg,
Details: details,
Cause: EncodeError(ctx, cause),
Message: msg,
Details: details,
MessageType: errorspb.MessageType(messageType),
},
},
}
@@ -158,21 +183,36 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
// extractPrefix extracts the prefix from a wrapper's error message.
// For example,
// err := errors.New("bar")
// err = errors.Wrap(err, "foo")
// extractPrefix(err)
//
// err := errors.New("bar")
// err = errors.Wrap(err, "foo")
// extractPrefix(err)
//
// returns "foo".
func extractPrefix(err, cause error) string {
//
// If a presumed wrapper does not have a message prefix, it is assumed
// to override the entire error message and `extractPrefix` returns
// the entire message and the boolean `true` to signify that the causes
// should not be appended to it.
func extractPrefix(err, cause error) (string, MessageType) {
causeSuffix := cause.Error()
errMsg := err.Error()
if strings.HasSuffix(errMsg, causeSuffix) {
prefix := errMsg[:len(errMsg)-len(causeSuffix)]
// If error msg matches exactly then this is a wrapper
// with no message of its own.
if len(prefix) == 0 {
return "", Prefix
}
if strings.HasSuffix(prefix, ": ") {
return prefix[:len(prefix)-2]
return prefix[:len(prefix)-2], Prefix
}
}
return ""
// If we don't have the cause as a suffix, then we have
// some other string as our error msg, preserve that and
// mark as override
return errMsg, FullMessage
}
func getTypeDetails(
@@ -183,6 +223,8 @@ func getTypeDetails(
switch t := err.(type) {
case *opaqueLeaf:
return t.details.OriginalTypeName, t.details.ErrorTypeMark.FamilyName, t.details.ErrorTypeMark.Extension
case *opaqueLeafCauses:
return t.details.OriginalTypeName, t.details.ErrorTypeMark.FamilyName, t.details.ErrorTypeMark.Extension
case *opaqueWrapper:
return t.details.OriginalTypeName, t.details.ErrorTypeMark.FamilyName, t.details.ErrorTypeMark.Extension
}
@@ -286,6 +328,28 @@ type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails
// registry for RegisterLeafEncoder.
var leafEncoders = map[TypeKey]LeafEncoder{}
// RegisterMultiCauseEncoder can be used to register new multi-cause
// error types to the library. Registered types will be encoded using
// their own Go type when an error is encoded. Multi-cause wrappers
// that have not been registered will be encoded using the
// opaqueWrapper type.
func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder) {
// This implementation is a simple wrapper around `LeafEncoder`
// because we implemented multi-cause error wrapper encoding into a
// `Leaf` instead of a `Wrapper` for smoother backwards
// compatibility support. Exposing this detail to consumers of the
// API is confusing and hence avoided. The causes of the error are
// encoded separately regardless of this encoder's implementation.
RegisterLeafEncoder(theType, encoder)
}
// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
// above) by additional multi-cause wrapper types not yet known to this
// library. The encoder will automatically extract and encode the
// causes of this error by calling `Unwrap()` and expecting a slice of
// errors.
type MultiCauseEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
// RegisterWrapperEncoder can be used to register new wrapper types to
// the library. Registered wrappers will be encoded using their own
// Go type when an error is encoded. Wrappers that have not been
@@ -295,6 +359,35 @@ var leafEncoders = map[TypeKey]LeafEncoder{}
// or a different type, ensure that RegisterTypeMigration() was called
// prior to RegisterWrapperEncoder().
func RegisterWrapperEncoder(theType TypeKey, encoder WrapperEncoder) {
RegisterWrapperEncoderWithMessageType(
theType,
func(ctx context.Context, err error) (
msgPrefix string,
safeDetails []string,
payload proto.Message,
messageType MessageType,
) {
prefix, details, payload := encoder(ctx, err)
return prefix, details, payload, messageType
})
}
// RegisterWrapperEncoderWithMessageType can be used to register
// new wrapper types to the library. Registered wrappers will be
// encoded using their own Go type when an error is encoded. Wrappers
// that have not been registered will be encoded using the
// opaqueWrapper type.
//
// This function differs from RegisterWrapperEncoder by allowing the
// caller to explicitly decide whether the wrapper owns the entire
// error message or not. Otherwise, the relationship is inferred.
//
// Note: if the error type has been migrated from a previous location
// or a different type, ensure that RegisterTypeMigration() was called
// prior to RegisterWrapperEncoder().
func RegisterWrapperEncoderWithMessageType(
theType TypeKey, encoder WrapperEncoderWithMessageType,
) {
if encoder == nil {
delete(encoders, theType)
} else {
@@ -304,7 +397,42 @@ func RegisterWrapperEncoder(theType TypeKey, encoder WrapperEncoder) {
// WrapperEncoder is to be provided (via RegisterWrapperEncoder above)
// by additional wrapper types not yet known to this library.
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message)
type WrapperEncoder func(ctx context.Context, err error) (
msgPrefix string,
safeDetails []string,
payload proto.Message,
)
// MessageType is used to encode information about an error message
// within a wrapper error type. This information is used to affect
// display logic.
type MessageType errorspb.MessageType
// Values below should match the ones in errorspb.MessageType for
// direct conversion.
const (
// Prefix denotes an error message that should be prepended to the
// message of its cause.
Prefix MessageType = MessageType(errorspb.MessageType_PREFIX)
// FullMessage denotes an error message that contains the text of its
// causes and can be displayed standalone.
FullMessage = MessageType(errorspb.MessageType_FULL_MESSAGE)
)
// WrapperEncoderWithMessageType is to be provided (via
// RegisterWrapperEncoderWithMessageType above) by additional wrapper
// types not yet known to this library. This encoder returns an
// additional enum which indicates whether the wrapper owns the error
// message completely instead of simply being a prefix with the error
// message of its causes appended to it. This information is encoded
// along with the prefix in order to provide context during error
// display.
type WrapperEncoderWithMessageType func(ctx context.Context, err error) (
msgPrefix string,
safeDetails []string,
payload proto.Message,
messageType MessageType,
)
// registry for RegisterWrapperType.
var encoders = map[TypeKey]WrapperEncoder{}
var encoders = map[TypeKey]WrapperEncoderWithMessageType{}

View File

@@ -102,7 +102,13 @@ func formatErrorInternal(err error, s fmt.State, verb rune, redactableOutput boo
// to enable stack trace de-duplication. This requires a
// post-order traversal. Since we have a linked list, the best we
// can do is a recursion.
p.formatRecursive(err, true /* isOutermost */, true /* withDetail */)
p.formatRecursive(
err,
true, /* isOutermost */
true, /* withDetail */
false, /* withDepth */
0, /* depth */
)
// We now have all the data, we can render the result.
p.formatEntries(err)
@@ -146,7 +152,13 @@ func formatErrorInternal(err error, s fmt.State, verb rune, redactableOutput boo
// by calling FormatError(), in which case we'd get an infinite
// recursion. So we have no choice but to peel the data
// and then assemble the pieces ourselves.
p.formatRecursive(err, true /* isOutermost */, false /* withDetail */)
p.formatRecursive(
err,
true, /* isOutermost */
false, /* withDetail */
false, /* withDepth */
0, /* depth */
)
p.formatSingleLineOutput()
p.finishDisplay(verb)
@@ -195,7 +207,19 @@ func (s *state) formatEntries(err error) {
// Wraps: (N) <details>
//
for i, j := len(s.entries)-2, 2; i >= 0; i, j = i-1, j+1 {
fmt.Fprintf(&s.finalBuf, "\nWraps: (%d)", j)
s.finalBuf.WriteByte('\n')
// Extra indentation starts at depth==2 because the direct
// children of the root error area already printed on separate
// newlines.
for m := 0; m < s.entries[i].depth-1; m += 1 {
if m == s.entries[i].depth-2 {
s.finalBuf.WriteString("└─ ")
} else {
s.finalBuf.WriteByte(' ')
s.finalBuf.WriteByte(' ')
}
}
fmt.Fprintf(&s.finalBuf, "Wraps: (%d)", j)
entry := s.entries[i]
s.printEntry(entry)
}
@@ -278,8 +302,9 @@ func (s *state) printEntry(entry formatEntry) {
//
// This function is used both when FormatError() is called indirectly
// from .Error(), e.g. in:
// (e *myType) Error() { return fmt.Sprintf("%v", e) }
// (e *myType) Format(s fmt.State, verb rune) { errors.FormatError(s, verb, e) }
//
// (e *myType) Error() { return fmt.Sprintf("%v", e) }
// (e *myType) Format(s fmt.State, verb rune) { errors.FormatError(s, verb, e) }
//
// and also to print the first line in the output of a %+v format.
//
@@ -329,13 +354,35 @@ func (s *state) formatSingleLineOutput() {
// s.finalBuf is untouched. The conversion of s.entries
// to s.finalBuf is done by formatSingleLineOutput() and/or
// formatEntries().
func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
//
// `withDepth` and `depth` are used to tag subtrees of multi-cause
// errors for added indentation during printing. Once a multi-cause
// error is encountered, all subsequent calls with set `withDepth` to
// true, and increment `depth` during recursion. This information is
// persisted into the generated entries and used later to display the
// error with increased indentation based in the depth.
func (s *state) formatRecursive(err error, isOutermost, withDetail, withDepth bool, depth int) int {
cause := UnwrapOnce(err)
numChildren := 0
if cause != nil {
// Recurse first.
s.formatRecursive(cause, false /*isOutermost*/, withDetail)
// Recurse first, which populates entries list starting from innermost
// entry. If we've previously seen a multi-cause wrapper, `withDepth`
// will be true, and we'll record the depth below ensuring that extra
// indentation is applied to this inner cause during printing.
// Otherwise, we maintain "straight" vertical formatting by keeping the
// parent callers `withDepth` value of `false` by default.
numChildren += s.formatRecursive(cause, false, withDetail, withDepth, depth+1)
}
causes := UnwrapMulti(err)
for _, c := range causes {
// Override `withDepth` to true for all child entries ensuring they have
// indentation applied during formatting to distinguish them from
// parents.
numChildren += s.formatRecursive(c, false, withDetail, true, depth+1)
}
// inserted := len(s.entries) - 1 - startChildren
// Reinitialize the state for this stage of wrapping.
s.wantDetail = withDetail
s.needSpace = false
@@ -349,75 +396,94 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
bufIsRedactable := false
printDone := false
for _, fn := range specialCases {
if handled, desiredShortening := fn(err, (*safePrinter)(s), cause == nil /* leaf */); handled {
printDone = true
bufIsRedactable = true
if desiredShortening == nil {
// The error wants to elide the short messages from inner
// causes. Do it.
for i := range s.entries {
s.entries[i].elideShort = true
}
}
break
switch v := err.(type) {
case SafeFormatter:
bufIsRedactable = true
desiredShortening := v.SafeFormatError((*safePrinter)(s))
if desiredShortening == nil {
// The error wants to elide the short messages from inner causes.
// Read backwards through list of entries up to the number of new
// entries created "under" this one amount and mark `elideShort`
// true.
s.elideShortChildren(numChildren)
}
}
if !printDone {
switch v := err.(type) {
case SafeFormatter:
bufIsRedactable = true
desiredShortening := v.SafeFormatError((*safePrinter)(s))
if desiredShortening == nil {
case Formatter:
desiredShortening := v.FormatError((*printer)(s))
if desiredShortening == nil {
// The error wants to elide the short messages from inner
// causes. Do it.
s.elideShortChildren(numChildren)
}
case fmt.Formatter:
// We can only use a fmt.Formatter when both the following
// conditions are true:
// - when it is the leaf error, because a fmt.Formatter
// on a wrapper also recurses.
// - when it is not the outermost wrapper, because
// the Format() method is likely to be calling FormatError()
// to do its job and we want to avoid an infinite recursion.
if !isOutermost && cause == nil {
v.Format(s, 'v')
if st, ok := err.(StackTraceProvider); ok {
// This is likely a leaf error from github/pkg/errors.
// The thing probably printed its stack trace on its own.
seenTrace = true
// We'll subsequently simplify stack traces in wrappers.
s.lastStack = st.StackTrace()
}
} else {
if elideCauseMsg := s.formatSimple(err, cause); elideCauseMsg {
// The error wants to elide the short messages from inner
// causes. Do it.
for i := range s.entries {
s.entries[i].elideShort = true
}
s.elideShortChildren(numChildren)
}
}
case Formatter:
desiredShortening := v.FormatError((*printer)(s))
if desiredShortening == nil {
// The error wants to elide the short messages from inner
// causes. Do it.
for i := range s.entries {
s.entries[i].elideShort = true
default:
// Handle the special case overrides for context.Canceled,
// os.PathError, etc for which we know how to extract some safe
// strings.
//
// We need to do this in the `default` branch, instead of doing
// this above the switch, because the special handler could call a
// .Error() that delegates its implementation to fmt.Formatter,
// errors.Safeformatter or errors.Formattable, which brings us
// back to this method in a call cycle. So we need to handle the
// various interfaces first.
printDone := false
for _, fn := range specialCases {
if handled, desiredShortening := fn(err, (*safePrinter)(s), cause == nil /* leaf */); handled {
printDone = true
bufIsRedactable = true
if desiredShortening == nil {
// The error wants to elide the short messages from inner
// causes. Do it.
s.elideShortChildren(numChildren)
}
break
}
case fmt.Formatter:
// We can only use a fmt.Formatter when both the following
// conditions are true:
// - when it is the leaf error, because a fmt.Formatter
// on a wrapper also recurses.
// - when it is not the outermost wrapper, because
// the Format() method is likely to be calling FormatError()
// to do its job and we want to avoid an infinite recursion.
if !isOutermost && cause == nil {
v.Format(s, 'v')
if st, ok := err.(StackTraceProvider); ok {
// This is likely a leaf error from github/pkg/errors.
// The thing probably printed its stack trace on its own.
seenTrace = true
// We'll subsequently simplify stack traces in wrappers.
s.lastStack = st.StackTrace()
}
} else {
s.formatSimple(err, cause)
}
default:
}
if !printDone {
// If the error did not implement errors.Formatter nor
// fmt.Formatter, but it is a wrapper, still attempt best effort:
// print what we can at this level.
s.formatSimple(err, cause)
elideChildren := s.formatSimple(err, cause)
// always elideChildren when dealing with multi-cause errors.
if len(causes) > 0 {
elideChildren = true
}
if elideChildren {
// The error wants to elide the short messages from inner
// causes. Do it.
s.elideShortChildren(numChildren)
}
}
}
// Collect the result.
entry := s.collectEntry(err, bufIsRedactable)
entry := s.collectEntry(err, bufIsRedactable, withDepth, depth)
// If there's an embedded stack trace, also collect it.
// This will get either a stack from pkg/errors, or ours.
@@ -431,9 +497,22 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
// Remember the entry for later rendering.
s.entries = append(s.entries, entry)
s.buf = bytes.Buffer{}
return numChildren + 1
}
func (s *state) collectEntry(err error, bufIsRedactable bool) formatEntry {
// elideShortChildren takes a number of entries to set `elideShort` to
// false. The reason a number of entries is needed is that we may be
// eliding a subtree of causes in the case of a multi-cause error. In
// the multi-cause case, we need to know how many of the prior errors
// in the list of entries is a child of this subtree.
func (s *state) elideShortChildren(newEntries int) {
for i := 0; i < newEntries; i++ {
s.entries[len(s.entries)-1-i].elideShort = true
}
}
func (s *state) collectEntry(err error, bufIsRedactable bool, withDepth bool, depth int) formatEntry {
entry := formatEntry{err: err}
if s.wantDetail {
// The buffer has been populated as a result of formatting with
@@ -470,6 +549,10 @@ func (s *state) collectEntry(err error, bufIsRedactable bool) formatEntry {
}
}
if withDepth {
entry.depth = depth
}
return entry
}
@@ -490,16 +573,23 @@ func RegisterSpecialCasePrinter(fn safeErrorPrinterFn) {
// formatSimple performs a best effort at extracting the details at a
// given level of wrapping when the error object does not implement
// the Formatter interface.
func (s *state) formatSimple(err, cause error) {
// Returns true if we want to elide errors from causal chain.
func (s *state) formatSimple(err, cause error) bool {
var pref string
elideCauses := false
if cause != nil {
pref = extractPrefix(err, cause)
var messageType MessageType
pref, messageType = extractPrefix(err, cause)
if messageType == FullMessage {
elideCauses = true
}
} else {
pref = err.Error()
}
if len(pref) > 0 {
s.Write([]byte(pref))
}
return elideCauses
}
// finishDisplay renders s.finalBuf into s.State.
@@ -558,7 +648,7 @@ type state struct {
// entries collect the result of formatRecursive(). They are
// consumed by formatSingleLineOutput() and formatEntries() to
// procude the contents of finalBuf.
// produce the contents of finalBuf.
entries []formatEntry
// buf collects the details of the current error object at a given
@@ -680,6 +770,11 @@ type formatEntry struct {
// truncated to avoid duplication of entries. This is used to
// display a truncation indicator during verbose rendering.
elidedStackTrace bool
// depth, if positive, represents a nesting depth of this error as
// a causer of others. This is used with verbose printing to
// illustrate the nesting depth for multi-cause error wrappers.
depth int
}
// String is used for debugging only.
@@ -701,6 +796,12 @@ func (s *state) Write(b []byte) (n int, err error) {
for i, c := range b {
if c == '\n' {
//if s.needNewline > 0 {
// for i := 0; i < s.needNewline-1; i++ {
// s.buf.Write(detailSep[:len(sep)-1])
// }
// s.needNewline = 0
//}
// Flush all the bytes seen so far.
s.buf.Write(b[k:i])
// Don't print the newline itself; instead, prepare the state so
@@ -730,6 +831,11 @@ func (s *state) Write(b []byte) (n int, err error) {
s.notEmpty = true
}
}
//if s.needNewline > 0 {
// for i := 0; i < s.needNewline-1; i++ {
// s.buf.Write(detailSep[:len(sep)-1])
// }
//}
s.buf.Write(b[k:])
return len(b), nil
}
@@ -756,6 +862,9 @@ func (p *state) switchOver() {
p.buf = bytes.Buffer{}
p.notEmpty = false
p.hasDetail = true
// One of the newlines is accounted for in the switch over.
// p.needNewline -= 1
}
func (s *printer) Detail() bool {
@@ -819,7 +928,7 @@ func (s *safePrinter) enhanceArgs(args []interface{}) {
args[i] = redact.Safe(thisStack)
lastSeen = st
}
// In contrast with (*printer).enhanceArgs(), we dont use a
// In contrast with (*printer).enhanceArgs(), we don't use a
// special case for `error` here, because the redact package
// already helps us recursing into a safe print for
// error objects.

View File

@@ -30,19 +30,39 @@ type opaqueLeaf struct {
details errorspb.EncodedErrorDetails
}
// opaqueLeafCauses is used when receiving an unknown multi-cause
// wrapper type. Its important property is that if it is communicated
// back to some network system that _does_ know about the type, the
// original object can be restored. We encode multi-cause errors as
// leaf nodes over the network, in order to support backwards
// compatibility with existing single-cause wrapper messages.
//
// This struct *must* be initialized with a non-nil causes value in
// order to comply with go stdlib expectations for `Unwrap()`.
type opaqueLeafCauses struct {
opaqueLeaf
causes []error
}
var _ error = (*opaqueLeaf)(nil)
var _ SafeDetailer = (*opaqueLeaf)(nil)
var _ fmt.Formatter = (*opaqueLeaf)(nil)
var _ SafeFormatter = (*opaqueLeaf)(nil)
var _ error = (*opaqueLeafCauses)(nil)
var _ SafeDetailer = (*opaqueLeafCauses)(nil)
var _ fmt.Formatter = (*opaqueLeafCauses)(nil)
var _ SafeFormatter = (*opaqueLeafCauses)(nil)
// opaqueWrapper is used when receiving an unknown wrapper type.
// Its important property is that if it is communicated
// back to some network system that _does_ know about
// the type, the original object can be restored.
type opaqueWrapper struct {
cause error
prefix string
details errorspb.EncodedErrorDetails
cause error
prefix string
details errorspb.EncodedErrorDetails
messageType MessageType
}
var _ error = (*opaqueWrapper)(nil)
@@ -53,6 +73,9 @@ var _ SafeFormatter = (*opaqueWrapper)(nil)
func (e *opaqueLeaf) Error() string { return e.msg }
func (e *opaqueWrapper) Error() string {
if e.messageType == FullMessage {
return e.prefix
}
if e.prefix == "" {
return e.cause.Error()
}
@@ -66,8 +89,12 @@ func (e *opaqueWrapper) Unwrap() error { return e.cause }
func (e *opaqueLeaf) SafeDetails() []string { return e.details.ReportablePayload }
func (e *opaqueWrapper) SafeDetails() []string { return e.details.ReportablePayload }
func (e *opaqueLeaf) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
func (e *opaqueWrapper) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
func (e *opaqueLeaf) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
func (e *opaqueLeafCauses) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
func (e *opaqueWrapper) Format(s fmt.State, verb rune) { FormatError(e, s, verb) }
// opaqueLeafCauses is a multi-cause wrapper
func (e *opaqueLeafCauses) Unwrap() []error { return e.causes }
func (e *opaqueLeaf) SafeFormatError(p Printer) (next error) {
p.Print(e.msg)
@@ -103,5 +130,8 @@ func (e *opaqueWrapper) SafeFormatError(p Printer) (next error) {
p.Printf("\npayload type: %s", redact.Safe(e.details.FullDetails.TypeUrl))
}
}
if e.messageType == FullMessage {
return nil
}
return e.cause
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Cockroach Authors.
// Copyright 2021 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,25 +12,13 @@
// implied. See the License for the specific language governing
// permissions and limitations under the License.
// +build go1.16
package errbase
import "io"
import "io/fs"
// formatSimpleError is a helper used by FormatError() for the top
// level error/wrapper argument, if it does not implement the
// errors.Formatter interface.
func formatSimpleError(err error, p *state, sep string) error {
if cause := UnwrapOnce(err); cause != nil {
pref := extractPrefix(err, cause)
p.buf.WriteString(pref)
if pref != "" {
p.buf.WriteByte(':')
p.buf.WriteString(sep)
}
err = cause
} else {
io.WriteString(&p.buf, err.Error())
err = nil
}
return err
func registerOsPathErrorMigration() {
// The os.PathError type was migrated to io.fs.PathError in Go 1.16.
RegisterTypeMigration("os", "*os.PathError", &fs.PathError{})
}

View File

@@ -0,0 +1,19 @@
// Copyright 2021 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
// +build !go1.16
package errbase
func registerOsPathErrorMigration() {}

View File

@@ -27,6 +27,16 @@ package errbase
// It supports both errors implementing causer (`Cause()` method, from
// github.com/pkg/errors) and `Wrapper` (`Unwrap()` method, from the
// Go 2 error proposal).
//
// UnwrapOnce treats multi-errors (those implementing the
// `Unwrap() []error` interface as leaf-nodes since they cannot
// reasonably be iterated through to a single cause. These errors
// are typically constructed as a result of `fmt.Errorf` which results
// in a `wrapErrors` instance that contains an interpolated error
// string along with a list of causes.
//
// The go stdlib does not define output on `Unwrap()` for a multi-cause
// error, so we default to nil here.
func UnwrapOnce(err error) (cause error) {
switch e := err.(type) {
case interface{ Cause() error }:
@@ -39,6 +49,7 @@ func UnwrapOnce(err error) (cause error) {
// UnwrapAll accesses the root cause object of the error.
// If the error has no cause (leaf error), it is returned directly.
// UnwrapAll treats multi-errors as leaf nodes.
func UnwrapAll(err error) error {
for {
if cause := UnwrapOnce(err); cause != nil {
@@ -49,3 +60,12 @@ func UnwrapAll(err error) error {
}
return err
}
// UnwrapMulti access the slice of causes that an error contains, if it is a
// multi-error.
func UnwrapMulti(err error) []error {
if me, ok := err.(interface{ Unwrap() []error }); ok {
return me.Unwrap()
}
return nil
}

View File

@@ -86,6 +86,20 @@ func GetTypeKey(err error) TypeKey { return errbase.GetTypeKey(err) }
// A nil return indicates that decoding was not successful.
type LeafDecoder = errbase.LeafDecoder
// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
// above) by additional multi-cause wrapper types not yet known by the
// library. A nil return indicates that decoding was not successful.
type MultiCauseDecoder = errbase.MultiCauseDecoder
// RegisterMultiCauseDecoder can be used to register new multi-cause
// wrapper types to the library. Registered wrappers will be decoded
// using their own Go type when an error is decoded. Multi-cause
// wrappers that have not been registered will be decoded using the
// opaqueWrapper type.
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
errbase.RegisterMultiCauseDecoder(theType, decoder)
}
// RegisterWrapperDecoder can be used to register new wrapper types to
// the library. Registered wrappers will be decoded using their own
// Go type when an error is decoded. Wrappers that have not been
@@ -135,6 +149,41 @@ func RegisterWrapperEncoder(typeName TypeKey, encoder WrapperEncoder) {
// by additional wrapper types not yet known to this library.
type WrapperEncoder = errbase.WrapperEncoder
// RegisterWrapperEncoderWithMessageType can be used to register new wrapper
// types to the library. These wrappers can optionally override the child error
// messages with their own error string instead of relying on iterative
// concatenation. Registered wrappers will be encoded using their own Go type
// when an error is encoded. Wrappers that have not been registered will be
// encoded using the opaqueWrapper type.
//
// Note: if the error type has been migrated from a previous location
// or a different type, ensure that RegisterTypeMigration() was called
// prior to RegisterWrapperEncoder().
func RegisterWrapperEncoderWithMessageType(typeName TypeKey, encoder WrapperEncoderWithMessageType) {
errbase.RegisterWrapperEncoderWithMessageType(typeName, encoder)
}
// WrapperEncoderWithMessageType is to be provided (via
// RegisterWrapperEncoderWithMessageType) by additional wrapper
// types not yet known to this library.
type WrapperEncoderWithMessageType = errbase.WrapperEncoderWithMessageType
// RegisterMultiCauseEncoder can be used to register new multi-cause
// error types to the library. Registered types will be encoded using
// their own Go type when an error is encoded. Multi-cause wrappers
// that have not been registered will be encoded using the
// opaqueWrapper type.
func RegisterMultiCauseEncoder(typeName TypeKey, encoder MultiCauseEncoder) {
errbase.RegisterMultiCauseEncoder(typeName, encoder)
}
// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
// above) by additional multi-cause wrapper types not yet known to this
// library. The encoder will automatically extract and encode the
// causes of this error by calling `Unwrap()` and expecting a slice of
// errors.
type MultiCauseEncoder = errbase.MultiCauseEncoder
// SetWarningFn enables configuration of the warning function.
func SetWarningFn(fn func(context.Context, string, ...interface{})) { errbase.SetWarningFn(fn) }
@@ -185,7 +234,8 @@ func Formattable(err error) fmt.Formatter { return errbase.Formattable(err) }
// The value of previousTypeName must be the result of calling
// reflect.TypeOf(err).String() on the original error object.
// This is usually composed as follows:
// [*]<shortpackage>.<errortype>
//
// [*]<shortpackage>.<errortype>
//
// For example, Go's standard error type has name "*errors.errorString".
// The asterisk indicates that `errorString` implements the `error`

View File

@@ -0,0 +1,21 @@
// Copyright 2021 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package errorspb
// IsSet returns true if the EncodedError contains an error, or false if it is
// empty.
func (m *EncodedError) IsSet() bool {
return m.Error != nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ message EncodedError {
}
// EncodedErrorLeaf is the wire-encodable representation
// of an error leaf.
// of an error leaf or a multi-cause wrapper from go 1.20+
message EncodedErrorLeaf {
// The main error message (mandatory), that can be printed to human
// users and may contain PII. This contains the value of the leaf
@@ -26,6 +26,12 @@ message EncodedErrorLeaf {
// The error details.
EncodedErrorDetails details = 2 [(gogoproto.nullable) = false];
// multierror_causes is a list of errors that contain the causal tree
// of this leaf. If this field is not empty, then this leaf encodes
// an error from go 1.20 or later that encodes multiple causes in its
// chain.
repeated EncodedError multierror_causes = 3;
}
message EncodedErrorDetails {
@@ -60,24 +66,42 @@ message EncodedErrorDetails {
google.protobuf.Any full_details = 4;
}
// MessageType encodes information regarding the interpretation of an
// error's message string.
enum MessageType {
// Keeping zero value as default for backwards compatibility.
// PREFIX is a message that should be prepended to its cause.
PREFIX = 0;
// FULL_MESSAGE is a complete error message that can be displayed
// without its cause.
FULL_MESSAGE = 1;
}
// EncodedWrapper is the wire-encodable representation
// of an error wrapper.
message EncodedWrapper {
// The cause error. Mandatory.
EncodedError cause = 1 [(gogoproto.nullable) = false];
// The wrapper message prefix (which may be empty). This
// isbprinted before the cause's own message when
// constructing a full message. This may contain PII.
// The wrapper message. This could either be a full error message
// that can be printed independently, or a (potentially empty) prefix
// which is printed before the cause's own message to construct the
// full message. This may contain PII.
//
// This is extracted automatically:
//
// - for wrappers that have a registered encoder,
// - otherwise, when the wrapper's Error() has its cause's Error() as suffix.
string message_prefix = 2;
string message = 2;
// The error details.
EncodedErrorDetails details = 3 [(gogoproto.nullable) = false];
// message_type encodes the interpretation of `message`. Prior
// versions will not set this field and it will be left as `PREFIX`.
// This retains backwards compatibility since the new behavior is
// only enabled when this enum is set to `FULL_MESSAGE`.
MessageType message_type = 4;
}
// ErrorTypeMark identifies an error type for the purpose of determining

View File

@@ -1,13 +1,15 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: github.com/cockroachdb/errors/errorspb/hintdetail.proto
// source: errorspb/hintdetail.proto
package errorspb
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
import (
fmt "fmt"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
@@ -18,7 +20,7 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type StringPayload struct {
Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"`
@@ -28,21 +30,21 @@ func (m *StringPayload) Reset() { *m = StringPayload{} }
func (m *StringPayload) String() string { return proto.CompactTextString(m) }
func (*StringPayload) ProtoMessage() {}
func (*StringPayload) Descriptor() ([]byte, []int) {
return fileDescriptor_hintdetail_429641491537a364, []int{0}
return fileDescriptor_d99b88ec9cc4bc22, []int{0}
}
func (m *StringPayload) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *StringPayload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
func (dst *StringPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_StringPayload.Merge(dst, src)
func (m *StringPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_StringPayload.Merge(m, src)
}
func (m *StringPayload) XXX_Size() int {
return m.Size()
@@ -56,10 +58,26 @@ var xxx_messageInfo_StringPayload proto.InternalMessageInfo
func init() {
proto.RegisterType((*StringPayload)(nil), "cockroach.errorspb.StringPayload")
}
func init() { proto.RegisterFile("errorspb/hintdetail.proto", fileDescriptor_d99b88ec9cc4bc22) }
var fileDescriptor_d99b88ec9cc4bc22 = []byte{
// 143 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4c, 0x2d, 0x2a, 0xca,
0x2f, 0x2a, 0x2e, 0x48, 0xd2, 0xcf, 0xc8, 0xcc, 0x2b, 0x49, 0x49, 0x2d, 0x49, 0xcc, 0xcc, 0xd1,
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x4a, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce,
0xd0, 0x83, 0x29, 0x52, 0x52, 0xe4, 0xe2, 0x0d, 0x2e, 0x29, 0xca, 0xcc, 0x4b, 0x0f, 0x48, 0xac,
0xcc, 0xc9, 0x4f, 0x4c, 0x11, 0x12, 0xe0, 0x62, 0xce, 0x2d, 0x4e, 0x97, 0x60, 0x54, 0x60, 0xd4,
0xe0, 0x0c, 0x02, 0x31, 0x9d, 0xb4, 0x4e, 0x3c, 0x94, 0x63, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2,
0x23, 0x39, 0xc6, 0x1b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1,
0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, 0xa2, 0x38, 0x60, 0xc6, 0x25, 0xb1, 0x81, 0x6d,
0x32, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xe2, 0xbd, 0x3a, 0x3e, 0x86, 0x00, 0x00, 0x00,
}
func (m *StringPayload) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -67,27 +85,35 @@ func (m *StringPayload) Marshal() (dAtA []byte, err error) {
}
func (m *StringPayload) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *StringPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.Msg) > 0 {
dAtA[i] = 0xa
i++
i -= len(m.Msg)
copy(dAtA[i:], m.Msg)
i = encodeVarintHintdetail(dAtA, i, uint64(len(m.Msg)))
i += copy(dAtA[i:], m.Msg)
i--
dAtA[i] = 0xa
}
return i, nil
return len(dAtA) - i, nil
}
func encodeVarintHintdetail(dAtA []byte, offset int, v uint64) int {
offset -= sovHintdetail(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
return base
}
func (m *StringPayload) Size() (n int) {
if m == nil {
@@ -103,14 +129,7 @@ func (m *StringPayload) Size() (n int) {
}
func sovHintdetail(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
return (math_bits.Len64(x|1) + 6) / 7
}
func sozHintdetail(x uint64) (n int) {
return sovHintdetail(uint64((x << 1) ^ uint64((int64(x) >> 63))))
@@ -130,7 +149,7 @@ func (m *StringPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -158,7 +177,7 @@ func (m *StringPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -168,6 +187,9 @@ func (m *StringPayload) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthHintdetail
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthHintdetail
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -179,7 +201,7 @@ func (m *StringPayload) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthHintdetail
}
if (iNdEx + skippy) > l {
@@ -197,6 +219,7 @@ func (m *StringPayload) Unmarshal(dAtA []byte) error {
func skipHintdetail(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
@@ -228,10 +251,8 @@ func skipHintdetail(dAtA []byte) (n int, err error) {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
@@ -248,72 +269,34 @@ func skipHintdetail(dAtA []byte) (n int, err error) {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthHintdetail
}
return iNdEx, nil
iNdEx += length
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowHintdetail
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipHintdetail(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
depth++
case 4:
return iNdEx, nil
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupHintdetail
}
depth--
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthHintdetail
}
if depth == 0 {
return iNdEx, nil
}
}
panic("unreachable")
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthHintdetail = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowHintdetail = fmt.Errorf("proto: integer overflow")
ErrInvalidLengthHintdetail = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowHintdetail = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupHintdetail = fmt.Errorf("proto: unexpected end of group")
)
func init() {
proto.RegisterFile("github.com/cockroachdb/errors/errorspb/hintdetail.proto", fileDescriptor_hintdetail_429641491537a364)
}
var fileDescriptor_hintdetail_429641491537a364 = []byte{
// 161 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x4f, 0xcf, 0x2c, 0xc9,
0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce,
0x48, 0x49, 0xd2, 0x4f, 0x2d, 0x2a, 0xca, 0x2f, 0x2a, 0x86, 0x52, 0x05, 0x49, 0xfa, 0x19, 0x99,
0x79, 0x25, 0x29, 0xa9, 0x25, 0x89, 0x99, 0x39, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x42,
0x70, 0xd5, 0x7a, 0x30, 0x45, 0x4a, 0x8a, 0x5c, 0xbc, 0xc1, 0x25, 0x45, 0x99, 0x79, 0xe9, 0x01,
0x89, 0x95, 0x39, 0xf9, 0x89, 0x29, 0x42, 0x02, 0x5c, 0xcc, 0xb9, 0xc5, 0xe9, 0x12, 0x8c, 0x0a,
0x8c, 0x1a, 0x9c, 0x41, 0x20, 0xa6, 0x93, 0xd6, 0x89, 0x87, 0x72, 0x0c, 0x27, 0x1e, 0xc9, 0x31,
0x5e, 0x78, 0x24, 0xc7, 0x78, 0xe3, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb,
0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x14, 0x07, 0xcc, 0xb8, 0x24, 0x36,
0xb0, 0x4d, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe0, 0x71, 0x36, 0x88, 0xa4, 0x00, 0x00,
0x00,
}

View File

@@ -1,13 +1,16 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: github.com/cockroachdb/errors/errorspb/markers.proto
// source: errorspb/markers.proto
package errorspb
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
import (
fmt "fmt"
_ "github.com/gogo/protobuf/gogoproto"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
@@ -18,7 +21,7 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// MarkPayload is the error payload for a forced marker.
// See errors/markers/markers.go and the RFC on
@@ -32,21 +35,21 @@ func (m *MarkPayload) Reset() { *m = MarkPayload{} }
func (m *MarkPayload) String() string { return proto.CompactTextString(m) }
func (*MarkPayload) ProtoMessage() {}
func (*MarkPayload) Descriptor() ([]byte, []int) {
return fileDescriptor_markers_2a35e57f2b6b5666, []int{0}
return fileDescriptor_84c3fd24ba37816d, []int{0}
}
func (m *MarkPayload) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *MarkPayload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
func (dst *MarkPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_MarkPayload.Merge(dst, src)
func (m *MarkPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_MarkPayload.Merge(m, src)
}
func (m *MarkPayload) XXX_Size() int {
return m.Size()
@@ -60,10 +63,30 @@ var xxx_messageInfo_MarkPayload proto.InternalMessageInfo
func init() {
proto.RegisterType((*MarkPayload)(nil), "cockroach.errorspb.MarkPayload")
}
func init() { proto.RegisterFile("errorspb/markers.proto", fileDescriptor_84c3fd24ba37816d) }
var fileDescriptor_84c3fd24ba37816d = []byte{
// 198 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4b, 0x2d, 0x2a, 0xca,
0x2f, 0x2a, 0x2e, 0x48, 0xd2, 0xcf, 0x4d, 0x2c, 0xca, 0x4e, 0x2d, 0x2a, 0xd6, 0x2b, 0x28, 0xca,
0x2f, 0xc9, 0x17, 0x12, 0x4a, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce, 0xd0, 0x83, 0xa9,
0x90, 0x12, 0x85, 0xab, 0x85, 0x30, 0x20, 0x4a, 0xa5, 0x44, 0xd2, 0xf3, 0xd3, 0xf3, 0xc1, 0x4c,
0x7d, 0x10, 0x0b, 0x22, 0xaa, 0x14, 0xc7, 0xc5, 0xed, 0x9b, 0x58, 0x94, 0x1d, 0x90, 0x58, 0x99,
0x93, 0x9f, 0x98, 0x22, 0x24, 0xc0, 0xc5, 0x9c, 0x5b, 0x9c, 0x2e, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1,
0x19, 0x04, 0x62, 0x0a, 0xd9, 0x72, 0xb1, 0x96, 0x54, 0x16, 0xa4, 0x16, 0x4b, 0x30, 0x29, 0x30,
0x6b, 0x70, 0x1b, 0x29, 0xea, 0x61, 0xda, 0xa8, 0xe7, 0x0a, 0x62, 0x84, 0x54, 0x16, 0xa4, 0x82,
0x8c, 0x72, 0x62, 0x39, 0x71, 0x4f, 0x9e, 0x21, 0x08, 0xa2, 0xcb, 0x49, 0xeb, 0xc4, 0x43, 0x39,
0x86, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0xbc, 0xf1, 0x48, 0x8e, 0xf1, 0xc1, 0x23,
0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x8a,
0x03, 0x66, 0x4c, 0x12, 0x1b, 0xd8, 0x49, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x18, 0x43,
0x45, 0xef, 0xed, 0x00, 0x00, 0x00,
}
func (m *MarkPayload) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -71,39 +94,49 @@ func (m *MarkPayload) Marshal() (dAtA []byte, err error) {
}
func (m *MarkPayload) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *MarkPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.Msg) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintMarkers(dAtA, i, uint64(len(m.Msg)))
i += copy(dAtA[i:], m.Msg)
}
if len(m.Types) > 0 {
for _, msg := range m.Types {
dAtA[i] = 0x12
i++
i = encodeVarintMarkers(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
for iNdEx := len(m.Types) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Types[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintMarkers(dAtA, i, uint64(size))
}
i += n
i--
dAtA[i] = 0x12
}
}
return i, nil
if len(m.Msg) > 0 {
i -= len(m.Msg)
copy(dAtA[i:], m.Msg)
i = encodeVarintMarkers(dAtA, i, uint64(len(m.Msg)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintMarkers(dAtA []byte, offset int, v uint64) int {
offset -= sovMarkers(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
return base
}
func (m *MarkPayload) Size() (n int) {
if m == nil {
@@ -125,14 +158,7 @@ func (m *MarkPayload) Size() (n int) {
}
func sovMarkers(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
return (math_bits.Len64(x|1) + 6) / 7
}
func sozMarkers(x uint64) (n int) {
return sovMarkers(uint64((x << 1) ^ uint64((int64(x) >> 63))))
@@ -152,7 +178,7 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -180,7 +206,7 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -190,6 +216,9 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthMarkers
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthMarkers
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -209,7 +238,7 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -218,6 +247,9 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthMarkers
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthMarkers
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -232,7 +264,7 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthMarkers
}
if (iNdEx + skippy) > l {
@@ -250,6 +282,7 @@ func (m *MarkPayload) Unmarshal(dAtA []byte) error {
func skipMarkers(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
@@ -281,10 +314,8 @@ func skipMarkers(dAtA []byte) (n int, err error) {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
@@ -301,75 +332,34 @@ func skipMarkers(dAtA []byte) (n int, err error) {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthMarkers
}
return iNdEx, nil
iNdEx += length
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMarkers
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipMarkers(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
depth++
case 4:
return iNdEx, nil
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupMarkers
}
depth--
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthMarkers
}
if depth == 0 {
return iNdEx, nil
}
}
panic("unreachable")
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthMarkers = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowMarkers = fmt.Errorf("proto: integer overflow")
ErrInvalidLengthMarkers = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowMarkers = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupMarkers = fmt.Errorf("proto: unexpected end of group")
)
func init() {
proto.RegisterFile("github.com/cockroachdb/errors/errorspb/markers.proto", fileDescriptor_markers_2a35e57f2b6b5666)
}
var fileDescriptor_markers_2a35e57f2b6b5666 = []byte{
// 216 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x49, 0xcf, 0x2c, 0xc9,
0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce,
0x48, 0x49, 0xd2, 0x4f, 0x2d, 0x2a, 0xca, 0x2f, 0x2a, 0x86, 0x52, 0x05, 0x49, 0xfa, 0xb9, 0x89,
0x45, 0xd9, 0xa9, 0x45, 0xc5, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x42, 0x70, 0xa5, 0x7a,
0x30, 0x15, 0x52, 0xc6, 0x44, 0x9a, 0x04, 0x61, 0x40, 0x0c, 0x92, 0x12, 0x49, 0xcf, 0x4f, 0xcf,
0x07, 0x33, 0xf5, 0x41, 0x2c, 0x88, 0xa8, 0x52, 0x1c, 0x17, 0xb7, 0x6f, 0x62, 0x51, 0x76, 0x40,
0x62, 0x65, 0x4e, 0x7e, 0x62, 0x8a, 0x90, 0x00, 0x17, 0x73, 0x6e, 0x71, 0xba, 0x04, 0xa3, 0x02,
0xa3, 0x06, 0x67, 0x10, 0x88, 0x29, 0x64, 0xcb, 0xc5, 0x5a, 0x52, 0x59, 0x90, 0x5a, 0x2c, 0xc1,
0xa4, 0xc0, 0xac, 0xc1, 0x6d, 0xa4, 0xa8, 0x87, 0xe9, 0x1e, 0x3d, 0x57, 0x10, 0x23, 0xa4, 0xb2,
0x20, 0x15, 0x64, 0x94, 0x13, 0xcb, 0x89, 0x7b, 0xf2, 0x0c, 0x41, 0x10, 0x5d, 0x4e, 0x5a, 0x27,
0x1e, 0xca, 0x31, 0x9c, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x8d, 0x47, 0x72, 0x8c,
0x0f, 0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72,
0x0c, 0x51, 0x1c, 0x30, 0x63, 0x92, 0xd8, 0xc0, 0x4e, 0x32, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff,
0xdc, 0x2d, 0x59, 0x6b, 0x29, 0x01, 0x00, 0x00,
}

View File

@@ -2,7 +2,7 @@ syntax = "proto3";
package cockroach.errorspb;
option go_package = "errorspb";
import "github.com/cockroachdb/errors/errorspb/errors.proto";
import "errorspb/errors.proto";
import "gogoproto/gogo.proto";
// MarkPayload is the error payload for a forced marker.

View File

@@ -1,13 +1,16 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: github.com/cockroachdb/errors/errorspb/tags.proto
// source: errorspb/tags.proto
package errorspb
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
import (
fmt "fmt"
_ "github.com/gogo/protobuf/gogoproto"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
@@ -18,7 +21,7 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// TagsPayload is the error payload for a WithContext
// marker.
@@ -32,21 +35,21 @@ func (m *TagsPayload) Reset() { *m = TagsPayload{} }
func (m *TagsPayload) String() string { return proto.CompactTextString(m) }
func (*TagsPayload) ProtoMessage() {}
func (*TagsPayload) Descriptor() ([]byte, []int) {
return fileDescriptor_tags_c75c5e322ef6b31c, []int{0}
return fileDescriptor_2f0d1dc5c54e9f63, []int{0}
}
func (m *TagsPayload) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *TagsPayload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
func (dst *TagsPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_TagsPayload.Merge(dst, src)
func (m *TagsPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_TagsPayload.Merge(m, src)
}
func (m *TagsPayload) XXX_Size() int {
return m.Size()
@@ -66,21 +69,21 @@ func (m *TagPayload) Reset() { *m = TagPayload{} }
func (m *TagPayload) String() string { return proto.CompactTextString(m) }
func (*TagPayload) ProtoMessage() {}
func (*TagPayload) Descriptor() ([]byte, []int) {
return fileDescriptor_tags_c75c5e322ef6b31c, []int{1}
return fileDescriptor_2f0d1dc5c54e9f63, []int{1}
}
func (m *TagPayload) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *TagPayload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
func (dst *TagPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_TagPayload.Merge(dst, src)
func (m *TagPayload) XXX_Merge(src proto.Message) {
xxx_messageInfo_TagPayload.Merge(m, src)
}
func (m *TagPayload) XXX_Size() int {
return m.Size()
@@ -95,10 +98,30 @@ func init() {
proto.RegisterType((*TagsPayload)(nil), "cockroach.errorspb.TagsPayload")
proto.RegisterType((*TagPayload)(nil), "cockroach.errorspb.TagPayload")
}
func init() { proto.RegisterFile("errorspb/tags.proto", fileDescriptor_2f0d1dc5c54e9f63) }
var fileDescriptor_2f0d1dc5c54e9f63 = []byte{
// 201 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4e, 0x2d, 0x2a, 0xca,
0x2f, 0x2a, 0x2e, 0x48, 0xd2, 0x2f, 0x49, 0x4c, 0x2f, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
0x12, 0x4a, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce, 0xd0, 0x83, 0x49, 0x4b, 0x89, 0xa4,
0xe7, 0xa7, 0xe7, 0x83, 0xa5, 0xf5, 0x41, 0x2c, 0x88, 0x4a, 0x25, 0x77, 0x2e, 0xee, 0x90, 0xc4,
0xf4, 0xe2, 0x80, 0xc4, 0xca, 0x9c, 0xfc, 0xc4, 0x14, 0x21, 0x0b, 0x2e, 0x16, 0x90, 0x31, 0x12,
0x8c, 0x0a, 0xcc, 0x1a, 0xdc, 0x46, 0x72, 0x7a, 0x98, 0xe6, 0xe8, 0x85, 0x24, 0xa6, 0x43, 0x55,
0x3b, 0xb1, 0x9c, 0xb8, 0x27, 0xcf, 0x10, 0x04, 0xd6, 0xa1, 0x64, 0xc2, 0xc5, 0x85, 0x90, 0x11,
0x12, 0xe0, 0x62, 0x2e, 0x49, 0x4c, 0x97, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x02, 0x31, 0x85,
0x44, 0xb8, 0x58, 0xcb, 0x12, 0x73, 0x4a, 0x53, 0x25, 0x98, 0xc0, 0x62, 0x10, 0x8e, 0x93, 0xd6,
0x89, 0x87, 0x72, 0x0c, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0x78, 0xe3, 0x91, 0x1c,
0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1,
0x1c, 0x43, 0x14, 0x07, 0xcc, 0xe2, 0x24, 0x36, 0xb0, 0x8b, 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff,
0xff, 0x0a, 0x90, 0xd1, 0xc9, 0xf2, 0x00, 0x00, 0x00,
}
func (m *TagsPayload) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -106,29 +129,36 @@ func (m *TagsPayload) Marshal() (dAtA []byte, err error) {
}
func (m *TagsPayload) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *TagsPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.Tags) > 0 {
for _, msg := range m.Tags {
dAtA[i] = 0xa
i++
i = encodeVarintTags(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
for iNdEx := len(m.Tags) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Tags[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintTags(dAtA, i, uint64(size))
}
i += n
i--
dAtA[i] = 0xa
}
}
return i, nil
return len(dAtA) - i, nil
}
func (m *TagPayload) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -136,33 +166,42 @@ func (m *TagPayload) Marshal() (dAtA []byte, err error) {
}
func (m *TagPayload) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *TagPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.Tag) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintTags(dAtA, i, uint64(len(m.Tag)))
i += copy(dAtA[i:], m.Tag)
}
if len(m.Value) > 0 {
dAtA[i] = 0x12
i++
i -= len(m.Value)
copy(dAtA[i:], m.Value)
i = encodeVarintTags(dAtA, i, uint64(len(m.Value)))
i += copy(dAtA[i:], m.Value)
i--
dAtA[i] = 0x12
}
return i, nil
if len(m.Tag) > 0 {
i -= len(m.Tag)
copy(dAtA[i:], m.Tag)
i = encodeVarintTags(dAtA, i, uint64(len(m.Tag)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintTags(dAtA []byte, offset int, v uint64) int {
offset -= sovTags(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
return base
}
func (m *TagsPayload) Size() (n int) {
if m == nil {
@@ -197,14 +236,7 @@ func (m *TagPayload) Size() (n int) {
}
func sovTags(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
return (math_bits.Len64(x|1) + 6) / 7
}
func sozTags(x uint64) (n int) {
return sovTags(uint64((x << 1) ^ uint64((int64(x) >> 63))))
@@ -224,7 +256,7 @@ func (m *TagsPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -252,7 +284,7 @@ func (m *TagsPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -261,6 +293,9 @@ func (m *TagsPayload) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthTags
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthTags
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -275,7 +310,7 @@ func (m *TagsPayload) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthTags
}
if (iNdEx + skippy) > l {
@@ -305,7 +340,7 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -333,7 +368,7 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -343,6 +378,9 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthTags
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthTags
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -362,7 +400,7 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -372,6 +410,9 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthTags
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthTags
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -383,7 +424,7 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthTags
}
if (iNdEx + skippy) > l {
@@ -401,6 +442,7 @@ func (m *TagPayload) Unmarshal(dAtA []byte) error {
func skipTags(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
@@ -432,10 +474,8 @@ func skipTags(dAtA []byte) (n int, err error) {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
@@ -452,75 +492,34 @@ func skipTags(dAtA []byte) (n int, err error) {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthTags
}
return iNdEx, nil
iNdEx += length
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowTags
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipTags(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
depth++
case 4:
return iNdEx, nil
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupTags
}
depth--
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthTags
}
if depth == 0 {
return iNdEx, nil
}
}
panic("unreachable")
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthTags = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowTags = fmt.Errorf("proto: integer overflow")
ErrInvalidLengthTags = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowTags = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupTags = fmt.Errorf("proto: unexpected end of group")
)
func init() {
proto.RegisterFile("github.com/cockroachdb/errors/errorspb/tags.proto", fileDescriptor_tags_c75c5e322ef6b31c)
}
var fileDescriptor_tags_c75c5e322ef6b31c = []byte{
// 218 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x4c, 0xcf, 0x2c, 0xc9,
0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce,
0x48, 0x49, 0xd2, 0x4f, 0x2d, 0x2a, 0xca, 0x2f, 0x2a, 0x86, 0x52, 0x05, 0x49, 0xfa, 0x25, 0x89,
0xe9, 0xc5, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x42, 0x70, 0x75, 0x7a, 0x30, 0x69, 0x29,
0x91, 0xf4, 0xfc, 0xf4, 0x7c, 0xb0, 0xb4, 0x3e, 0x88, 0x05, 0x51, 0xa9, 0xe4, 0xce, 0xc5, 0x1d,
0x92, 0x98, 0x5e, 0x1c, 0x90, 0x58, 0x99, 0x93, 0x9f, 0x98, 0x22, 0x64, 0xc1, 0xc5, 0x02, 0x32,
0x46, 0x82, 0x51, 0x81, 0x59, 0x83, 0xdb, 0x48, 0x4e, 0x0f, 0xd3, 0x1c, 0xbd, 0x90, 0xc4, 0x74,
0xa8, 0x6a, 0x27, 0x96, 0x13, 0xf7, 0xe4, 0x19, 0x82, 0xc0, 0x3a, 0x94, 0x4c, 0xb8, 0xb8, 0x10,
0x32, 0x42, 0x02, 0x5c, 0xcc, 0x25, 0x89, 0xe9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x20,
0xa6, 0x90, 0x08, 0x17, 0x6b, 0x59, 0x62, 0x4e, 0x69, 0xaa, 0x04, 0x13, 0x58, 0x0c, 0xc2, 0x71,
0xd2, 0x3a, 0xf1, 0x50, 0x8e, 0xe1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x6f, 0x3c,
0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, 0x6e,
0x3c, 0x96, 0x63, 0x88, 0xe2, 0x80, 0x59, 0x9c, 0xc4, 0x06, 0x76, 0xb1, 0x31, 0x20, 0x00, 0x00,
0xff, 0xff, 0xa6, 0x1b, 0x01, 0x30, 0x10, 0x01, 0x00, 0x00,
}

View File

@@ -1,13 +1,15 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: github.com/cockroachdb/errors/errorspb/testing.proto
// source: errorspb/testing.proto
package errorspb
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
import (
fmt "fmt"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
@@ -18,7 +20,7 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// TestError is meant for use in testing only.
type TestError struct {
@@ -28,21 +30,21 @@ func (m *TestError) Reset() { *m = TestError{} }
func (m *TestError) String() string { return proto.CompactTextString(m) }
func (*TestError) ProtoMessage() {}
func (*TestError) Descriptor() ([]byte, []int) {
return fileDescriptor_testing_d9751a0a5e86c096, []int{0}
return fileDescriptor_0551f0d913d6118f, []int{0}
}
func (m *TestError) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *TestError) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
func (dst *TestError) XXX_Merge(src proto.Message) {
xxx_messageInfo_TestError.Merge(dst, src)
func (m *TestError) XXX_Merge(src proto.Message) {
xxx_messageInfo_TestError.Merge(m, src)
}
func (m *TestError) XXX_Size() int {
return m.Size()
@@ -56,10 +58,25 @@ var xxx_messageInfo_TestError proto.InternalMessageInfo
func init() {
proto.RegisterType((*TestError)(nil), "cockroach.errorspb.TestError")
}
func init() { proto.RegisterFile("errorspb/testing.proto", fileDescriptor_0551f0d913d6118f) }
var fileDescriptor_0551f0d913d6118f = []byte{
// 118 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4b, 0x2d, 0x2a, 0xca,
0x2f, 0x2a, 0x2e, 0x48, 0xd2, 0x2f, 0x49, 0x2d, 0x2e, 0xc9, 0xcc, 0x4b, 0xd7, 0x2b, 0x28, 0xca,
0x2f, 0xc9, 0x17, 0x12, 0x4a, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce, 0xd0, 0x83, 0xa9,
0x50, 0xe2, 0xe6, 0xe2, 0x0c, 0x49, 0x2d, 0x2e, 0x71, 0x05, 0xf1, 0x9d, 0xb4, 0x4e, 0x3c, 0x94,
0x63, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x1b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c,
0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, 0xa2,
0x38, 0x60, 0x1a, 0x93, 0xd8, 0xc0, 0x66, 0x1a, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x4b, 0xf3,
0x3e, 0x92, 0x6d, 0x00, 0x00, 0x00,
}
func (m *TestError) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -67,21 +84,28 @@ func (m *TestError) Marshal() (dAtA []byte, err error) {
}
func (m *TestError) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *TestError) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
return i, nil
return len(dAtA) - i, nil
}
func encodeVarintTesting(dAtA []byte, offset int, v uint64) int {
offset -= sovTesting(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
return base
}
func (m *TestError) Size() (n int) {
if m == nil {
@@ -93,14 +117,7 @@ func (m *TestError) Size() (n int) {
}
func sovTesting(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
return (math_bits.Len64(x|1) + 6) / 7
}
func sozTesting(x uint64) (n int) {
return sovTesting(uint64((x << 1) ^ uint64((int64(x) >> 63))))
@@ -120,7 +137,7 @@ func (m *TestError) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -140,7 +157,7 @@ func (m *TestError) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthTesting
}
if (iNdEx + skippy) > l {
@@ -158,6 +175,7 @@ func (m *TestError) Unmarshal(dAtA []byte) error {
func skipTesting(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
@@ -189,10 +207,8 @@ func skipTesting(dAtA []byte) (n int, err error) {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
@@ -209,70 +225,34 @@ func skipTesting(dAtA []byte) (n int, err error) {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthTesting
}
return iNdEx, nil
iNdEx += length
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowTesting
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipTesting(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
depth++
case 4:
return iNdEx, nil
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupTesting
}
depth--
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthTesting
}
if depth == 0 {
return iNdEx, nil
}
}
panic("unreachable")
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthTesting = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowTesting = fmt.Errorf("proto: integer overflow")
ErrInvalidLengthTesting = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowTesting = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupTesting = fmt.Errorf("proto: unexpected end of group")
)
func init() {
proto.RegisterFile("github.com/cockroachdb/errors/errorspb/testing.proto", fileDescriptor_testing_d9751a0a5e86c096)
}
var fileDescriptor_testing_d9751a0a5e86c096 = []byte{
// 136 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x49, 0xcf, 0x2c, 0xc9,
0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0x4f, 0xce, 0x2e, 0xca, 0x4f, 0x4c, 0xce,
0x48, 0x49, 0xd2, 0x4f, 0x2d, 0x2a, 0xca, 0x2f, 0x2a, 0x86, 0x52, 0x05, 0x49, 0xfa, 0x25, 0xa9,
0xc5, 0x25, 0x99, 0x79, 0xe9, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x42, 0x70, 0xa5, 0x7a,
0x30, 0x15, 0x4a, 0xdc, 0x5c, 0x9c, 0x21, 0xa9, 0xc5, 0x25, 0xae, 0x20, 0xbe, 0x93, 0xd6, 0x89,
0x87, 0x72, 0x0c, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0x78, 0xe3, 0x91, 0x1c, 0xe3,
0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c,
0x43, 0x14, 0x07, 0x4c, 0x63, 0x12, 0x1b, 0xd8, 0x4c, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff,
0x8a, 0x2c, 0x50, 0x5f, 0x8b, 0x00, 0x00, 0x00,
}

View File

@@ -58,7 +58,16 @@ func As(err error, target interface{}) bool {
if x, ok := c.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// If at any point in the single cause chain including the top,
// we encounter a multi-cause chain, recursively explore it.
for _, cause := range errbase.UnwrapMulti(c) {
if As(cause, target) {
return true
}
}
}
return false
}

View File

@@ -14,6 +14,6 @@
// Package errutil combines primitives from the library, the Go errors
// package and github.com/pkg/errors. It aims to serve as drop-in
// replacement to gitub.com/pkg/errors and replace the legacy
// replacement to github.com/pkg/errors and replace the legacy
// implementation of `pgerrors`.
package errutil

View File

@@ -15,6 +15,7 @@
package errutil
import (
"github.com/cockroachdb/errors/join"
"github.com/cockroachdb/errors/secondary"
"github.com/cockroachdb/errors/withstack"
"github.com/cockroachdb/redact"
@@ -158,3 +159,10 @@ func WrapWithDepthf(depth int, err error, format string, args ...interface{}) er
err = withstack.WithStackDepth(err, depth+1)
return err
}
// JoinWithDepth constructs a Join error with the provided list of
// errors as arguments, and wraps it in a `WithStackDepth` to capture a
// stacktrace alongside.
func JoinWithDepth(depth int, errs ...error) error {
return withstack.WithStackDepth(join.Join(errs...), depth+1)
}

View File

@@ -192,3 +192,19 @@ func HandleAsAssertionFailureDepth(depth int, origErr error) error {
// - it also supports recursing through causes with Cause().
// - if it detects an API use error, its panic object is a valid error.
func As(err error, target interface{}) bool { return errutil.As(err, target) }
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if errs contains no non-nil values.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string. A stack trace is also retained.
func Join(errs ...error) error {
return errutil.JoinWithDepth(1, errs...)
}
// JoinWithDepth is like Join but the depth at which the call stack is
// captured can be specified.
func JoinWithDepth(depth int, errs ...error) error {
return errutil.JoinWithDepth(depth+1, errs...)
}

View File

@@ -23,6 +23,7 @@ import (
// WithHint decorates an error with a textual hint.
// The hint may contain PII and thus will not reportable.
// The suggested use case for hint is to relay information to end users.
//
// Hint is shown:
// - when formatting with `%+v`.
@@ -97,10 +98,12 @@ type ErrorHinter interface {
// WithDetail decorates an error with a textual detail.
// The detail may contain PII and thus will not reportable.
// The suggested use case for detail is to augment errors with information
// useful for debugging.
//
// Detail is shown:
// - when formatting with `%+v`.
// - with `GetAllHints()` / `FlattenHints()` below.
// - with `GetAllDetails()` / `FlattenDetails()` below.
//
// Note: the detail does not appear in the main error message returned
// with Error(). Use GetAllDetails() or FlattenDetails() to retrieve

View File

@@ -27,6 +27,7 @@ type ErrorDetailer = hintdetail.ErrorDetailer
// WithHint decorates an error with a textual hint.
// The hint may contain PII and thus will not reportable.
// The suggested use case for hint is to relay information to end users.
//
// Hint is shown:
// - when formatting with `%+v`.
@@ -44,10 +45,12 @@ func WithHintf(err error, format string, args ...interface{}) error {
// WithDetail decorates an error with a textual detail.
// The detail may contain PII and thus will not reportable.
// The suggested use case for detail is to augment errors with information
// useful for debugging.
//
// Detail is shown:
// - when formatting with `%+v`.
// - with `GetAllHints()` / `FlattenHints()` below.
// - with `GetAllDetails()` / `FlattenDetails()` below.
//
// Note: the detail does not appear in the main error message returned
// with Error(). Use GetAllDetails() or FlattenDetails() to retrieve

105
vendor/github.com/cockroachdb/errors/join/join.go generated vendored Normal file
View File

@@ -0,0 +1,105 @@
// Copyright 2019 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package join
import (
"context"
"fmt"
"github.com/cockroachdb/errors/errbase"
"github.com/cockroachdb/redact"
"github.com/gogo/protobuf/proto"
)
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if errs contains no non-nil values.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &joinError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
type joinError struct {
errs []error
}
var _ error = (*joinError)(nil)
var _ fmt.Formatter = (*joinError)(nil)
var _ errbase.SafeFormatter = (*joinError)(nil)
func (e *joinError) Error() string {
return redact.Sprint(e).StripMarkers()
}
func (e *joinError) Unwrap() []error {
return e.errs
}
func (e *joinError) SafeFormatError(p errbase.Printer) error {
for i, err := range e.errs {
if i > 0 {
p.Print("\n")
}
p.Print(err)
}
return nil
}
func (e *joinError) Format(s fmt.State, verb rune) {
errbase.FormatError(e, s, verb)
}
func init() {
errbase.RegisterMultiCauseEncoder(
errbase.GetTypeKey(&joinError{}),
func(
ctx context.Context,
err error,
) (msg string, safeDetails []string, payload proto.Message) {
return "", nil, nil
},
)
errbase.RegisterMultiCauseDecoder(
errbase.GetTypeKey(&joinError{}),
func(
ctx context.Context,
causes []error,
msgPrefix string,
safeDetails []string,
payload proto.Message,
) error {
return Join(causes...)
},
)
}

View File

@@ -28,20 +28,44 @@ import (
// Is determines whether one of the causes of the given error or any
// of its causes is equivalent to some reference error.
//
// As in the Go standard library, an error is considered to match a
// reference error if it is equal to that target or if it implements a
// method Is(error) bool such that Is(reference) returns true.
//
// Note: the inverse is not true - making an Is(reference) method
// return false does not imply that errors.Is() also returns
// false. Errors can be equal because their network equality marker is
// the same. To force errors to appear different to Is(), use
// errors.Mark().
//
// Note: if any of the error types has been migrated from a previous
// package location or a different type, ensure that
// RegisterTypeMigration() was called prior to Is().
func Is(err, reference error) bool {
if reference == nil {
return err == reference
return err == nil
}
isComparable := reflect.TypeOf(reference).Comparable()
// Direct reference comparison is the fastest, and most
// likely to be true, so do this first.
for c := err; c != nil; c = errbase.UnwrapOnce(c) {
if c == reference {
if isComparable && c == reference {
return true
}
// Compatibility with std go errors: if the error object itself
// implements Is(), try to use that.
if tryDelegateToIsMethod(c, reference) {
return true
}
// Recursively try multi-error causes, if applicable.
for _, me := range errbase.UnwrapMulti(c) {
if Is(me, reference) {
return true
}
}
}
if err == nil {
@@ -51,8 +75,8 @@ func Is(err, reference error) bool {
return false
}
// Not directly equal. Try harder, using error marks. We don't this
// during the loop above as it may be more expensive.
// Not directly equal. Try harder, using error marks. We don't do
// this during the loop above as it may be more expensive.
//
// Note: there is a more effective recursive algorithm that ensures
// that any pair of string only gets compared once. Should the
@@ -67,6 +91,13 @@ func Is(err, reference error) bool {
return false
}
func tryDelegateToIsMethod(err, reference error) bool {
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(reference) {
return true
}
return false
}
// HasType returns true iff err contains an error whose concrete type
// matches that of referenceType.
func HasType(err error, referenceType error) bool {
@@ -119,21 +150,12 @@ func If(err error, pred func(err error) (interface{}, bool)) (interface{}, bool)
// package location or a different type, ensure that
// RegisterTypeMigration() was called prior to IsAny().
func IsAny(err error, references ...error) bool {
// First try using direct reference comparison.
for c := err; ; c = errbase.UnwrapOnce(c) {
if err == nil {
for _, refErr := range references {
if c == refErr {
if refErr == nil {
return true
}
}
if c == nil {
// This special case is to support a comparison to a nil
// reference.
break
}
}
if err == nil {
// The mark-based comparison below will never match anything if
// the error is nil, so don't bother with computing the marks in
// that case. This avoids the computational expense of computing
@@ -141,6 +163,31 @@ func IsAny(err error, references ...error) bool {
return false
}
// First try using direct reference comparison.
for c := err; c != nil; c = errbase.UnwrapOnce(c) {
for _, refErr := range references {
if refErr == nil {
continue
}
isComparable := reflect.TypeOf(refErr).Comparable()
if isComparable && c == refErr {
return true
}
// Compatibility with std go errors: if the error object itself
// implements Is(), try to use that.
if tryDelegateToIsMethod(c, refErr) {
return true
}
}
// Recursively try multi-error causes, if applicable.
for _, me := range errbase.UnwrapMulti(c) {
if IsAny(me, references...) {
return true
}
}
}
// Try harder with marks.
// Note: there is a more effective recursive algorithm that ensures
// that any pair of string only gets compared once. Should this

View File

@@ -19,6 +19,16 @@ import "github.com/cockroachdb/errors/markers"
// Is determines whether one of the causes of the given error or any
// of its causes is equivalent to some reference error.
//
// As in the Go standard library, an error is considered to match a
// reference error if it is equal to that target or if it implements a
// method Is(error) bool such that Is(reference) returns true.
//
// Note: the inverse is not true - making an Is(reference) method
// return false does not imply that errors.Is() also returns
// false. Errors can be equal because their network equality marker is
// the same. To force errors to appear different to Is(), use
// errors.Mark().
//
// Note: if any of the error types has been migrated from a previous
// package location or a different type, ensure that
// RegisterTypeMigration() was called prior to Is().

View File

@@ -15,7 +15,6 @@
package report
import (
"bytes"
"fmt"
"strings"
@@ -23,7 +22,7 @@ import (
"github.com/cockroachdb/errors/errbase"
"github.com/cockroachdb/errors/withstack"
"github.com/cockroachdb/redact"
"github.com/cockroachdb/sentry-go"
sentry "github.com/getsentry/sentry-go"
)
// BuildSentryReport builds the components of a sentry report. This
@@ -35,36 +34,39 @@ import (
//
// A Sentry report is displayed visually in the Sentry UI as follows:
//
////////////////
// //////////////
// Title: (1) some prefix in bold (2) one line for a stack trace
// (3) a single-line subtitle
//
// (4) the tags, as a tag soup (concatenated in a single paragrah,
// (4) the tags, as a tag soup (concatenated in a single paragraph,
// unsorted)
//
// (5) a "message"
//
// (6) zero or more "exceptions", each composed of:
// (7) a bold title
// (8) some freeform text
// (9) a stack trace
//
// (7) a bold title
// (8) some freeform text
// (9) a stack trace
//
// (10) metadata fields: environment, arch, etc
//
// (11) "Additional data" fields
//
// (12) SDK version
/////////////////
// ///////////////
//
// These visual items map to the Sentry Event object as follows:
//
// (1) the Type field of the 1st Exception object, if any
// otherwise the Message field
//
// otherwise the Message field
//
// (2) the topmost entry from the Stacktrace field of the 1st Exception object, if any
// (3) the Value field of the 1st Exception object, if any, unwrapped as a single line
// (4) the Tags field
// (5) the Message field
// (7) the Type field (same as (1) for 1st execption)
// (7) the Type field (same as (1) for 1st exception)
// (8) the Value field (same as (3) for 1st exception)
// (9) the Stacktrace field (input to (2) on 1st exception)
// (10) the other fields on the Event object
@@ -76,13 +78,15 @@ import (
// Given this mapping, an error object is decomposed as follows:
//
// (1)/(7): <filename>:<lineno> (<functionname>)
// (3)/(8): <error type>: <first safe detail line, if any>
// (3)/(8): first line of verbose error printout
// (4): not populated in this function, caller is to manage this
// (5): detailed structure of the entire error object, with references to "additional data"
// and additional "exception" objects
// (5): detailed structure of the entire error object, with references to
//
// additional "exception" objects
//
// (9): generated from innermost stack trace
// (6): every exception object after the 1st reports additional stack trace contexts
// (11): "additional data" populated from safe detail payloads
// (11): the detailed error types and their error mark.
//
// If there is no stack trace in the error, a synthetic Exception
// object is still produced to provide visual detail in the Sentry UI.
@@ -93,7 +97,6 @@ import (
// is included in the Sentry report. This does not affect error types
// provided by the library, but could impact error types defined by
// 3rd parties. This limitation may be lifted in a later version.
//
func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]interface{}) {
if err == nil {
// No error: do nothing.
@@ -103,13 +106,13 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
// First step is to collect the details.
var stacks []*withstack.ReportableStackTrace
var details []errbase.SafeDetailPayload
for c := err; c != nil; c = errbase.UnwrapOnce(c) {
visitAllMulti(err, func(c error) {
st := withstack.GetReportableStackTrace(c)
stacks = append(stacks, st)
sd := errbase.GetSafeDetails(c)
details = append(details, sd)
}
})
module := string(domains.GetDomain(err))
// firstDetailLine is the first detail string encountered.
@@ -119,17 +122,22 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
var firstDetailLine string
// longMsgBuf will become the Message field, which contains the full
// structure of the error with cross-references to "Exception" and
// "Additional data" fields.
// structure of the error.
var longMsgBuf strings.Builder
redactedErrStr := redact.Sprint(err).Redact()
if redactedErrStr != redactedMarker {
if f, l, _, ok := withstack.GetOneLineSource(err); ok {
fmt.Fprintf(&longMsgBuf, "%s:%d: ", f, l)
}
firstDetailLine = redactedErrStr.StripMarkers()
fmt.Fprintf(&longMsgBuf, "%v\n--\n", firstDetailLine)
if f, l, _, ok := withstack.GetOneLineSource(err); ok {
fmt.Fprintf(&longMsgBuf, "%s:%d: ", f, l)
}
// Include the verbose error printout, with sensitive bits redacted out.
verboseErr := redact.Sprintf("%+v", err).Redact().StripMarkers()
if verboseErr != redactedMarker {
idx := strings.IndexByte(verboseErr, '\n')
if idx == -1 {
firstDetailLine = verboseErr
} else {
firstDetailLine = verboseErr[:idx]
}
}
fmt.Fprint(&longMsgBuf, verboseErr)
// sep is used to separate the entries in the longMsgBuf / Message
// payload.
@@ -159,6 +167,7 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
// layer. We iterate in this order because we want to describe the
// error from innermost to outermost layer in longMsgBuf and
// typesBuf.
longMsgBuf.WriteString("\n-- report composition:\n")
for i := len(details) - 1; i >= 0; i-- {
// Collect the type name for this layer of error wrapping, towards
// the "error types" additional data field.
@@ -204,10 +213,6 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
// Now decide what kind of payload we want to add to the Event
// object.
// genExtra will remember whether we are adding an
// additional payload or not.
var genExtra bool
// Is there a stack trace?
if st := stacks[i]; st != nil {
var excType strings.Builder
@@ -245,17 +250,21 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
exceptions = append(exceptions, exc)
} else {
// No stack trace.
// Are there safe details? If so, print them.
// Are there safe details? If so, print the first safe detail
// string (we're assuming that all the important bits will
// also be included in the verbose printout, so there's no
// need to dig out more safe strings here.)
//
// TODO(knz): the SafeDetails API is not really meant for Sentry
// reporting. Once we have more experience to prove that the
// verbose printout is sufficient, we can remove the SafeDetails
// from sentry reports.
//
// Note: we only print the details if no stack trace was found
// at that level. This is because stack trace annotations also
// produce the stack trace as safe detail string.
genExtra = len(details[i].SafeDetails) > 1
if len(details[i].SafeDetails) > 0 {
d := details[i].SafeDetails[0]
if d != "" {
genExtra = true
}
if j := strings.IndexByte(d, '\n'); j >= 0 {
d = d[:j]
}
@@ -269,18 +278,6 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
}
}
}
// Are we generating another extra for the safe detail strings?
if genExtra {
stKey := fmt.Sprintf("%d: details", extraNum)
var extraStr bytes.Buffer
for _, d := range details[i].SafeDetails {
fmt.Fprintln(&extraStr, strings.ReplaceAll(d, "\n", "\n "))
}
extras[stKey] = extraStr.String()
fmt.Fprintf(&longMsgBuf, " (%d)", extraNum)
extraNum++
}
}
if extraNum > 1 {
@@ -299,7 +296,7 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
event.Message = longMsgBuf.String()
event.Exception = exceptions
// If there is no exception payload, synthetize one.
// If there is no exception payload, synthesize one.
if len(event.Exception) == 0 {
// We know we don't have a stack trace to extract line/function
// info from (if we had, we'd have an Exception payload at that
@@ -354,7 +351,7 @@ func BuildSentryReport(err error) (event *sentry.Event, extraDetails map[string]
return event, extras
}
var redactedMarker = redact.RedactableString(redact.RedactedMarker())
var redactedMarker = redact.RedactableString(redact.RedactedMarker()).StripMarkers()
// ReportError reports the given error to Sentry. The caller is responsible for
// checking whether telemetry is enabled, and calling the sentry.Flush()
@@ -404,3 +401,13 @@ func reverseExceptionOrder(ex []sentry.Exception) {
ex[i], ex[len(ex)-i-1] = ex[len(ex)-i-1], ex[i]
}
}
func visitAllMulti(err error, f func(error)) {
f(err)
if e := errbase.UnwrapOnce(err); e != nil {
visitAllMulti(e, f)
}
for _, e := range errbase.UnwrapMulti(err) {
visitAllMulti(e, f)
}
}

View File

@@ -18,7 +18,7 @@ import (
"bytes"
"fmt"
"github.com/cockroachdb/sentry-go"
"github.com/getsentry/sentry-go"
)
// StackTrace is an object suitable for inclusion in errors that can

View File

@@ -16,7 +16,7 @@ package errors
import (
"github.com/cockroachdb/errors/report"
"github.com/cockroachdb/sentry-go"
"github.com/getsentry/sentry-go"
)
// BuildSentryReport builds the components of a sentry report. This
@@ -32,7 +32,7 @@ import (
// Title: (1) some prefix in bold (2) one line for a stack trace
// (3) a single-line subtitle
//
// (4) the tags, as a tag soup (concatenated in a single paragrah,
// (4) the tags, as a tag soup (concatenated in a single paragraph,
// unsorted)
//
// (5) a "message"
@@ -57,7 +57,7 @@ import (
// (3) the Value field of the 1st Exception object, if any, unwrapped as a single line
// (4) the Tags field
// (5) the Message field
// (7) the Type field (same as (1) for 1st execption)
// (7) the Type field (same as (1) for 1st exception)
// (8) the Value field (same as (3) for 1st exception)
// (9) the Stacktrace field (input to (2) on 1st exception)
// (10) the other fields on the Event object

View File

@@ -23,7 +23,7 @@ import (
"strings"
"github.com/cockroachdb/errors/errbase"
"github.com/cockroachdb/sentry-go"
"github.com/getsentry/sentry-go"
pkgErr "github.com/pkg/errors"
)

View File

@@ -1,9 +1,6 @@
.vscode/
.idea/
.DS_Store
coverage.txt
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
@@ -14,12 +11,15 @@ coverage.txt
# Test binary, built with `go test -c`
*.test
# Test example output
example.logV2.hlog
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
.idea/

201
vendor/github.com/cockroachdb/fifo/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

21
vendor/github.com/cockroachdb/fifo/invariants_off.go generated vendored Normal file
View File

@@ -0,0 +1,21 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//go:build !fifo_invariants
package fifo
// invariants is false if we were not built with the "fifo_invariants" build
// tag.
const invariants = false

20
vendor/github.com/cockroachdb/fifo/invariants_on.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//go:build fifo_invariants
package fifo
// invariants is true if we were built with the "fifo_invariants" build tag.
const invariants = true

162
vendor/github.com/cockroachdb/fifo/queue.go generated vendored Normal file
View File

@@ -0,0 +1,162 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package fifo
import "sync"
// Queue implements an allocation efficient FIFO queue. It is not safe for
// concurrent access.
//
// Note that the queue provides pointer access to the internal storage (via
// PeekFront and PushBack) so it must be used with care. These pointers must not
// be used once the respective element is popped out of the queue.
//
// -- Implementation --
//
// The queue is implemented as a linked list of nodes, where each node is a
// small ring buffer. The nodes are allocated using a sync.Pool (a single pool
// is created for any given type and is used for all queues of that type).
type Queue[T any] struct {
len int
head, tail *queueNode[T]
pool *QueueBackingPool[T]
}
// MakeQueue constructs a new Queue.
//
// The pool should be a singleton object initialized with MakeQueueBackingPool.
// A single pool can and should be used by all queues of that type.
func MakeQueue[T any](pool *QueueBackingPool[T]) Queue[T] {
return Queue[T]{
pool: pool,
}
}
// Len returns the current length of the queue.
func (q *Queue[T]) Len() int {
return q.len
}
// PushBack adds t to the end of the queue.
//
// The returned pointer can be used to modify the element while it is in the
// queue; it is valid until the element is removed from the queue.
func (q *Queue[T]) PushBack(t T) *T {
if q.head == nil {
q.head = q.pool.get()
q.tail = q.head
} else if q.tail.IsFull() {
newTail := q.pool.get()
q.tail.next = newTail
q.tail = newTail
}
q.len++
return q.tail.PushBack(t)
}
// PeekFront returns the current head of the queue, or nil if the queue is
// empty.
//
// The result is only valid until the next call to PopFront.
func (q *Queue[T]) PeekFront() *T {
if q.len == 0 {
return nil
}
return q.head.PeekFront()
}
// PopFront removes the current head of the queue.
//
// It is illegal to call PopFront on an empty queue.
func (q *Queue[T]) PopFront() {
q.head.PopFront()
// If this is the only node, we don't want to release it; otherwise we would
// allocate/free a node every time we transition between the queue being empty
// and non-empty.
if q.head.len == 0 && q.head.next != nil {
oldHead := q.head
q.head = oldHead.next
q.pool.put(oldHead)
}
q.len--
}
// QueueBackingPool is a sync.Pool that used to allocate internal nodes
// for Queue[T].
type QueueBackingPool[T any] struct {
pool sync.Pool
}
// MakeQueueBackingPool makes a queue backing pool. It is intented to be used to
// initialize a singleton (global) variable. A single pool can and should be
// used by all queues of that type.
func MakeQueueBackingPool[T any]() QueueBackingPool[T] {
return QueueBackingPool[T]{
pool: sync.Pool{
New: func() interface{} { return &queueNode[T]{} },
},
}
}
func (qp *QueueBackingPool[T]) get() *queueNode[T] {
return qp.pool.Get().(*queueNode[T])
}
func (qp *QueueBackingPool[T]) put(n *queueNode[T]) {
*n = queueNode[T]{}
qp.pool.Put(n)
}
// We batch the allocation of this many queue objects. The value was chosen
// without experimentation - it provides a reasonable amount of amortization
// without a very large increase in memory overhead if T is large.
const queueNodeSize = 8
type queueNode[T any] struct {
buf [queueNodeSize]T
head, len int32
next *queueNode[T]
}
func (qn *queueNode[T]) IsFull() bool {
return qn.len == queueNodeSize
}
func (qn *queueNode[T]) PushBack(t T) *T {
if invariants && qn.len >= queueNodeSize {
panic("cannot push back into a full node")
}
i := (qn.head + qn.len) % queueNodeSize
qn.buf[i] = t
qn.len++
return &qn.buf[i]
}
func (qn *queueNode[T]) PeekFront() *T {
return &qn.buf[qn.head]
}
func (qn *queueNode[T]) PopFront() T {
if invariants && qn.len == 0 {
panic("cannot pop from empty queue")
}
t := qn.buf[qn.head]
var zero T
qn.buf[qn.head] = zero
qn.head = (qn.head + 1) % queueNodeSize
qn.len--
return t
}

239
vendor/github.com/cockroachdb/fifo/semaphore.go generated vendored Normal file
View File

@@ -0,0 +1,239 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package fifo
import (
"context"
"errors"
"fmt"
"sync"
)
// Semaphore implements a weighted, dynamically reconfigurable semaphore which
// respects context cancellation.
//
// The semaphore implements a FIFO policy, where Acquire requests are satisfied
// in order. This policy provides fairness and prevents starvation but is
// susceptible to head-of-line blocking, where a large request that can't be
// satisfied blocks many other small requests that could be.
type Semaphore struct {
mu struct {
sync.Mutex
capacity int64
// outstanding can exceed capacity if the capacity is dynamically decreased.
outstanding int64
waiters Queue[semaWaiter]
// numCanceled is the number of waiters in the waiters queue which have been
// canceled. It is used to determine the current number of active waiters in
// the queue which is waiters.Len() minus this value.
numCanceled int
// numHadToWait accumulates the total number of Acquire requests which had
// to wait because the semaphore was exhausted.
numHadToWait int64
}
}
// NewSemaphore creates a new semaphore with the given capacity.
func NewSemaphore(capacity int64) *Semaphore {
if capacity <= 0 {
panic("invalid capacity")
}
s := &Semaphore{}
s.mu.capacity = capacity
s.mu.waiters = MakeQueue[semaWaiter](&semaQueuePool)
return s
}
var semaQueuePool = MakeQueueBackingPool[semaWaiter]()
var ErrRequestExceedsCapacity = errors.New("request exceeds semaphore capacity")
// TryAcquire attempts to acquire n units from the semaphore without waiting. On
// success, returns true and the caller must later Release the units.
func (s *Semaphore) TryAcquire(n int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
s.mu.outstanding += n
return true
}
return false
}
// Acquire n units from the semaphore, waiting if necessary.
//
// If the context is canceled while we are waiting, returns the context error.
// If n exceeds the current capacity, returns ErrRequestExceedsCapacity.
// On success, the caller must later Release the units.
func (s *Semaphore) Acquire(ctx context.Context, n int64) error {
s.mu.Lock()
// Fast path.
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
s.mu.outstanding += n
s.mu.Unlock()
return nil
}
if n > s.mu.capacity {
s.mu.Unlock()
return ErrRequestExceedsCapacity
}
c := chanSyncPool.Get().(chan error)
defer chanSyncPool.Put(c)
w := s.mu.waiters.PushBack(semaWaiter{n: n, c: c})
s.mu.numHadToWait++
s.mu.Unlock()
select {
case <-ctx.Done():
s.mu.Lock()
defer s.mu.Unlock()
// We need to check if we raced with a channel notify (which happens under
// the lock).
select {
case err := <-c:
// We actually fulfilled or failed the request.
return err
default:
}
// Mark the request as canceled.
w.c = nil
s.mu.numCanceled++
// If we are the head of the queue, we may be able to fulfill other waiters.
s.processWaitersLocked()
return ctx.Err()
case err := <-c:
return err
}
}
// Release n units back. These must be units that were acquired by a previous
// Acquire call. It is legal to split up or coalesce units when releasing.
func (s *Semaphore) Release(n int64) {
s.mu.Lock()
defer s.mu.Unlock()
s.mu.outstanding -= n
if s.mu.outstanding < 0 {
panic("releasing more than was acquired")
}
s.processWaitersLocked()
}
// UpdateCapacity changes the capacity of the semaphore. If the new capacity is
// smaller, the already outstanding acquisitions might exceed the new capacity
// until they are released.
//
// If there are Acquire calls that are waiting which are requesting more than
// the new capacity, they will error out with ErrRequestExceedsCapacity.
func (s *Semaphore) UpdateCapacity(capacity int64) {
if capacity <= 0 {
panic("invalid capacity")
}
s.mu.Lock()
defer s.mu.Unlock()
s.mu.capacity = capacity
s.processWaitersLocked()
}
// Stats returns the current state of the semaphore.
func (s *Semaphore) Stats() SemaphoreStats {
s.mu.Lock()
defer s.mu.Unlock()
return SemaphoreStats{
Capacity: s.mu.capacity,
Outstanding: s.mu.outstanding,
NumHadToWait: s.mu.numHadToWait,
}
}
// SemaphoreStats contains information about the current state of the semaphore.
type SemaphoreStats struct {
// Capacity is the current capacity of the semaphore.
Capacity int64
// Outstanding is the number of units that have been acquired. Note that this
// can exceed Capacity if the capacity was recently decreased.
Outstanding int64
// NumHadToWait is the total number of Acquire calls (since the semaphore was
// created) that had to wait because the semaphore was exhausted. Useful for
// cumulative metrics.
NumHadToWait int64
// TODO(radu): consider keeping track of the total amount of time the
// semaphore was exhausted (i.e. there were waiters queued).
}
func (ss SemaphoreStats) String() string {
return fmt.Sprintf("capacity: %d, outstanding: %d, num-had-to-wait: %d",
ss.Capacity, ss.Outstanding, ss.NumHadToWait)
}
type semaWaiter struct {
// n is the amount that the waiter is trying to acquire.
n int64
// c is the channel on which Acquire is blocked. If the request is canceled,
// it is set to nil.
c chan error
}
// numWaitersLocked returns how many requests (that have not been canceled) are
// waiting in the queue.
func (s *Semaphore) numWaitersLocked() int {
return s.mu.waiters.Len() - s.mu.numCanceled
}
// processWaitersLocked processes and notifies as many waiters from the head of
// the queue as possible.
func (s *Semaphore) processWaitersLocked() {
for ; s.mu.waiters.Len() > 0; s.mu.waiters.PopFront() {
switch w := s.mu.waiters.PeekFront(); {
case w.c == nil:
// Request was canceled, we can just clean it up.
s.mu.numCanceled--
if invariants && s.mu.numCanceled < 0 {
panic("negative numCanceled")
}
case s.mu.outstanding+w.n <= s.mu.capacity:
// Request can be fulfilled.
s.mu.outstanding += w.n
w.c <- nil
case w.n > s.mu.capacity:
// Request must be failed. This can happen if the capacity was decreased
// while the element was queued.
w.c <- ErrRequestExceedsCapacity
default:
// Head of the queue needs to wait some more.
return
}
}
}
// chanSyncPool is used to pool allocations of the channels used to notify
// goroutines waiting in Acquire.
var chanSyncPool = sync.Pool{
New: func() interface{} { return make(chan error, 1) },
}

View File

@@ -38,6 +38,20 @@ func (b *Buffer) Get() []Tag {
return b.tags
}
// GetTag returns the tag corresponding to the given key. If the tag doesn't
// exist, the bool return value will be false.
func (b *Buffer) GetTag(key string) (Tag, bool) {
if b == nil {
return Tag{}, false
}
for _, t := range b.tags {
if t.Key() == key {
return t, true
}
}
return Tag{}, false
}
// Add returns a new buffer with one more tag. If the tag has the same key as an
// earlier tag, that tag is overwritten.
// The receiver can be nil.
@@ -52,6 +66,25 @@ func (b *Buffer) Add(key string, value interface{}) *Buffer {
return res
}
// Remove returns a new Buffer with the tag with key `key` removed. If the tag
// does not exist, the receiver is returned (unchanged). The bool return value
// is true if the tag existed.
func (b *Buffer) Remove(key string) (*Buffer, bool) {
if b == nil {
return nil, false
}
for i, t := range b.tags {
if t.Key() == key {
res := &Buffer{}
res.init(len(b.tags)-1, 0 /* capacityHint */)
copy(res.tags, b.tags[:i])
copy(res.tags[i:], b.tags[i+1:])
return res, true
}
}
return b, false
}
// Merge returns a new buffer which contains tags from the receiver, followed by
// the tags from <other>.
//
@@ -67,7 +100,7 @@ func (b *Buffer) Merge(other *Buffer) *Buffer {
if b == nil || len(b.tags) == 0 {
return other
}
if len(other.tags) == 0 {
if other == nil || len(other.tags) == 0 {
return b
}
@@ -121,12 +154,12 @@ func (b *Buffer) String() string {
}
// FormatToString emits the k/v pairs to a strings.Builder.
// - the k/v pairs are separated by commas (& no spaces).
// - if there is no value, only the key is printed.
// - if there is a value, and the key is just 1 character long,
// the key and the value are concatenated.
// This supports e.g. printing k="n", v=123 as "n123".
// - otherwise, it prints "k=v".
// - the k/v pairs are separated by commas (& no spaces).
// - if there is no value, only the key is printed.
// - if there is a value, and the key is just 1 character long,
// the key and the value are concatenated.
// This supports e.g. printing k="n", v=123 as "n123".
// - otherwise, it prints "k=v".
func (b *Buffer) FormatToString(buf *strings.Builder) {
comma := ""
for _, t := range b.Get() {
@@ -142,13 +175,19 @@ func (b *Buffer) FormatToString(buf *strings.Builder) {
}
}
func (b *Buffer) init(length, maxLenHint int) {
// init initializes b.tags to a slice of len `length` (filled with zero values).
// The slice may have capacity capacityHint. If capacityHint is less than
// length, it is ignored.
func (b *Buffer) init(length, capacityHint int) {
if length <= staticSlots {
// Even if maxLenHint is larger that staticSlots, we still want to try to
// Even if capacityHint is larger that staticSlots, we still want to try to
// avoid the allocation (especially since tags frequently get deduplicated).
b.tags = b.prealloc[:length]
} else {
b.tags = make([]Tag, length, maxLenHint)
if capacityHint < length {
capacityHint = length
}
b.tags = make([]Tag, length, capacityHint)
}
}

View File

@@ -41,6 +41,18 @@ func AddTag(ctx context.Context, key string, value interface{}) context.Context
return WithTags(ctx, b.Add(key, value))
}
// RemoveTag returns a context that has the tags in the given context except the
// tag with key `key`. If such a tag does not exist, the given context is
// returned.
func RemoveTag(ctx context.Context, key string) context.Context {
b := FromContext(ctx)
newB, ok := b.Remove(key)
if !ok {
return ctx
}
return WithTags(ctx, newB)
}
// AddTags returns a context that has the tags in the given context plus another
// set of tags. Tags are deduplicated (see Buffer.AddTags).
func AddTags(ctx context.Context, tags *Buffer) context.Context {

View File

@@ -1,4 +1,9 @@
# Github action artifacts.
artifacts
# Profiling artifacts.
cpu.*.prof
heap.prof
mutex.prof
coverprofile.out
# Testing artifacts
meta.*.test

View File

@@ -4,7 +4,8 @@ GOFLAGS :=
STRESSFLAGS :=
TAGS := invariants
TESTS := .
LATEST_RELEASE := $(shell git fetch origin && git branch -r --list '*/crl-release-*' | grep -o 'crl-release-.*$$' | sort | tail -1)
PREV_RELEASE := crl-release-23.1
COVER_PROFILE := coverprofile.out
.PHONY: all
all:
@@ -15,18 +16,40 @@ all:
@echo " make stressrace"
@echo " make stressmeta"
@echo " make crossversion-meta"
@echo " make testcoverage"
@echo " make mod-update"
@echo " make generate"
@echo " make generate-test-data"
@echo " make clean"
override testflags :=
.PHONY: test
test:
${GO} test -mod=vendor -tags '$(TAGS)' ${testflags} -run ${TESTS} ${PKG}
${GO} test -tags '$(TAGS)' ${testflags} -run ${TESTS} ${PKG}
.PHONY: testcoverage
testcoverage:
${GO} test -tags '$(TAGS)' ${testflags} -run ${TESTS} ${PKG} -coverprofile ${COVER_PROFILE}
.PHONY: testrace
testrace: testflags += -race -timeout 20m
testrace: test
testasan: testflags += -asan -timeout 20m
testasan: test
testmsan: export CC=clang
testmsan: testflags += -msan -timeout 20m
testmsan: test
.PHONY: testobjiotracing
testobjiotracing:
${GO} test -tags '$(TAGS) pebble_obj_io_tracing' ${testflags} -run ${TESTS} ./objstorage/objstorageprovider/objiotracing
.PHONY: lint
lint:
${GO} test -tags '$(TAGS)' ${testflags} -run ${TESTS} ./internal/lint
.PHONY: stress stressrace
stressrace: testflags += -race
stress stressrace: testflags += -exec 'stress ${STRESSFLAGS}' -timeout 0 -test.v
@@ -40,20 +63,34 @@ stressmeta: stress
.PHONY: crossversion-meta
crossversion-meta:
git checkout ${LATEST_RELEASE}; \
${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/${LATEST_RELEASE}.test'; \
git checkout ${PREV_RELEASE}; \
${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/${PREV_RELEASE}.test'; \
git checkout -; \
${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/head.test'; \
${GO} test -tags '$(TAGS)' ${testflags} -v -run 'TestMetaCrossVersion' ./internal/metamorphic/crossversion --version '${LATEST_RELEASE},${LATEST_RELEASE},${LATEST_RELEASE}.test' --version 'HEAD,HEAD,./head.test'
${GO} test -tags '$(TAGS)' ${testflags} -v -run 'TestMetaCrossVersion' ./internal/metamorphic/crossversion --version '${PREV_RELEASE},${PREV_RELEASE},${PREV_RELEASE}.test' --version 'HEAD,HEAD,./head.test'
.PHONY: stress-crossversion
stress-crossversion:
STRESS=1 ./scripts/run-crossversion-meta.sh crl-release-21.2 crl-release-22.1 crl-release-22.2 crl-release-23.1 master
.PHONY: generate
generate:
${GO} generate -mod=vendor ${PKG}
${GO} generate ${PKG}
generate:
# Note that the output of generate-test-data is not deterministic. This should
# only be run manually as needed.
.PHONY: generate-test-data
generate-test-data:
${GO} run -tags make_incorrect_manifests ./tool/make_incorrect_manifests.go
${GO} run -tags make_test_find_db ./tool/make_test_find_db.go
${GO} run -tags make_test_sstables ./tool/make_test_sstables.go
${GO} run -tags make_test_remotecat ./tool/make_test_remotecat.go
mod-update:
${GO} get -u
${GO} mod tidy
${GO} mod vendor
.PHONY: clean
clean:
@@ -78,11 +115,11 @@ endif
@${GO} mod tidy
$(MAKE) git-clean-check
# TODO(radu): switch back to @latest once bogus doc changes are
# addressed; see https://github.com/cockroachdb/crlfmt/pull/44
.PHONY: format
format:
for _file in $$(gofmt -s -l . | grep -vE '^vendor/'); do \
gofmt -s -w $$_file ; \
done
go install github.com/cockroachdb/crlfmt@44a36ec7 && crlfmt -w -tab 2 .
.PHONY: format-check
format-check:
@@ -90,4 +127,5 @@ ifneq ($(git_dirty),)
$(error format-check must be invoked on a clean repository)
endif
$(MAKE) format
git diff
$(MAKE) git-clean-check

View File

@@ -1,4 +1,4 @@
# Pebble [![Build Status](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml) [![GoDoc](https://godoc.org/github.com/cockroachdb/pebble?status.svg)](https://godoc.org/github.com/cockroachdb/pebble)
# Pebble [![Build Status](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml) [![GoDoc](https://godoc.org/github.com/cockroachdb/pebble?status.svg)](https://godoc.org/github.com/cockroachdb/pebble) <sup><sub><sub>[Coverage](https://storage.googleapis.com/crl-codecover-public/pebble/index.html)</sub></sub></sup>
#### [Nightly benchmarks](https://cockroachdb.github.io/pebble/)
@@ -8,9 +8,8 @@ RocksDB file formats and a few extensions such as range deletion
tombstones, table-level bloom filters, and updates to the MANIFEST
format.
Pebble intentionally does not aspire to include every feature in
RocksDB and is specifically targetting the use case and feature set
needed by CockroachDB:
Pebble intentionally does not aspire to include every feature in RocksDB and
specifically targets the use case and feature set needed by CockroachDB:
* Block-based tables
* Checkpoints
@@ -68,6 +67,17 @@ Pebble offers several improvements over RocksDB:
* Faster commit pipeline that achieves better concurrency.
* Seamless merged iteration of indexed batches. The mutations in the
batch conceptually occupy another memtable level.
* L0 sublevels and flush splitting for concurrent compactions out of L0 and
reduced read-amplification during heavy write load.
* Faster LSM edits in LSMs with large numbers of sstables through use of a
copy-on-write B-tree to hold file metadata.
* Delete-only compactions that drop whole sstables that fall within the bounds
of a range deletion.
* Block-property collectors and filters that enable iterators to skip tables,
index blocks and data blocks that are irrelevant, according to user-defined
properties over key-value pairs.
* Range keys API, allowing KV pairs defined over a range of keyspace with
user-defined semantics and interleaved during iteration.
* Smaller, more approachable code base.
See the [Pebble vs RocksDB: Implementation
@@ -100,10 +110,68 @@ known incompatibilities.
format.
* Plain table format. Pebble does not support the plain table sstable
format.
* SSTable format version 3 and 4. Pebble does not currently support
version 3 and version 4 format sstables. The sstable format version
is controlled by the `BlockBasedTableOptions::format_version`
option. See [#97](https://github.com/cockroachdb/pebble/issues/97).
* SSTable format version 3 and 4. Pebble does not support version 3
and version 4 format sstables. The sstable format version is
controlled by the `BlockBasedTableOptions::format_version` option.
See [#97](https://github.com/cockroachdb/pebble/issues/97).
## Format major versions
Over time Pebble has introduced new physical file formats. Backwards
incompatible changes are made through the introduction of 'format major
versions'. By default, when Pebble opens a database, it defaults to
`FormatMostCompatible`. This version is bi-directionally compatible with RocksDB
6.2.1 (with the caveats described above).
To opt into new formats, a user may set `FormatMajorVersion` on the
[`Options`](https://pkg.go.dev/github.com/cockroachdb/pebble#Options)
supplied to
[`Open`](https://pkg.go.dev/github.com/cockroachdb/pebble#Open), or
upgrade the format major version at runtime using
[`DB.RatchetFormatMajorVersion`](https://pkg.go.dev/github.com/cockroachdb/pebble#DB.RatchetFormatMajorVersion).
Format major version upgrades are permanent; There is no option to
return to an earlier format.
The table below outlines the history of format major versions:
| Name | Value | Migration |
|------------------------------------|-------|------------|
| FormatMostCompatible | 1 | No |
| FormatVersioned | 3 | No |
| FormatSetWithDelete | 4 | No |
| FormatBlockPropertyCollector | 5 | No |
| FormatSplitUserKeysMarked | 6 | Background |
| FormatSplitUserKeysMarkedCompacted | 7 | Blocking |
| FormatRangeKeys | 8 | No |
| FormatMinTableFormatPebblev1 | 9 | No |
| FormatPrePebblev1Marked | 10 | Background |
| FormatSSTableValueBlocks | 12 | No |
| FormatFlushableIngest | 13 | No |
| FormatPrePebblev1MarkedCompacted | 14 | Blocking |
| FormatDeleteSizedAndObsolete | 15 | No |
| FormatVirtualSSTables | 16 | No |
Upgrading to a format major version with 'Background' in the migration
column may trigger background activity to rewrite physical file
formats, typically through compactions. Upgrading to a format major
version with 'Blocking' in the migration column will block until a
migration is complete. The database may continue to serve reads and
writes if upgrading a live database through
`RatchetFormatMajorVersion`, but the method call will not return until
the migration is complete.
For reference, the table below lists the range of supported Pebble format major
versions for CockroachDB releases.
| CockroachDB release | Earliest supported | Latest supported |
|---------------------|------------------------------------|---------------------------|
| 20.1 through 21.1 | FormatMostCompatible | FormatMostCompatible |
| 21.2 | FormatMostCompatible | FormatSetWithDelete |
| 21.2 | FormatMostCompatible | FormatSetWithDelete |
| 22.1 | FormatMostCompatible | FormatSplitUserKeysMarked |
| 22.2 | FormatMostCompatible | FormatPrePebblev1Marked |
| 23.1 | FormatSplitUserKeysMarkedCompacted | FormatFlushableIngest |
| 23.2 | FormatSplitUserKeysMarkedCompacted | FormatVirtualSSTables |
## Pedigree

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,9 @@ type Cache = cache.Cache
// creator of the cache should usually release their reference after the DB is
// created.
//
// c := pebble.NewCache(...)
// defer c.Unref()
// d, err := pebble.Open(pebble.Options{Cache: c})
// c := pebble.NewCache(...)
// defer c.Unref()
// d, err := pebble.Open(pebble.Options{Cache: c})
func NewCache(size int64) *cache.Cache {
return cache.New(size)
}

View File

@@ -5,10 +5,12 @@
package pebble
import (
"io"
"os"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/record"
"github.com/cockroachdb/pebble/vfs"
"github.com/cockroachdb/pebble/vfs/atomicfs"
)
@@ -19,6 +21,9 @@ type checkpointOptions struct {
// flushWAL set to true will force a flush and sync of the WAL prior to
// checkpointing.
flushWAL bool
// If set, any SSTs that don't overlap with these spans are excluded from a checkpoint.
restrictToSpans []CheckpointSpan
}
// CheckpointOption set optional parameters used by `DB.Checkpoint`.
@@ -39,6 +44,43 @@ func WithFlushedWAL() CheckpointOption {
}
}
// WithRestrictToSpans specifies spans of interest for the checkpoint. Any SSTs
// that don't overlap with any of these spans are excluded from the checkpoint.
//
// Note that the checkpoint can still surface keys outside of these spans (from
// the WAL and from SSTs that partially overlap with these spans). Moreover,
// these surface keys aren't necessarily "valid" in that they could have been
// modified but the SST containing the modification is excluded.
func WithRestrictToSpans(spans []CheckpointSpan) CheckpointOption {
return func(opt *checkpointOptions) {
opt.restrictToSpans = spans
}
}
// CheckpointSpan is a key range [Start, End) (inclusive on Start, exclusive on
// End) of interest for a checkpoint.
type CheckpointSpan struct {
Start []byte
End []byte
}
// excludeFromCheckpoint returns true if an SST file should be excluded from the
// checkpoint because it does not overlap with the spans of interest
// (opt.restrictToSpans).
func excludeFromCheckpoint(f *fileMetadata, opt *checkpointOptions, cmp Compare) bool {
if len(opt.restrictToSpans) == 0 {
// Option not set; don't exclude anything.
return false
}
for _, s := range opt.restrictToSpans {
if f.Overlaps(cmp, s.Start, s.End, true /* exclusiveEnd */) {
return false
}
}
// None of the restrictToSpans overlapped; we can exclude this file.
return true
}
// mkdirAllAndSyncParents creates destDir and any of its missing parents.
// Those missing parents, as well as the closest existing ancestor, are synced.
// Returns a handle to the directory created at destDir.
@@ -46,23 +88,20 @@ func mkdirAllAndSyncParents(fs vfs.FS, destDir string) (vfs.File, error) {
// Collect paths for all directories between destDir (excluded) and its
// closest existing ancestor (included).
var parentPaths []string
foundExistingAncestor := false
for parentPath := fs.PathDir(destDir); parentPath != "."; parentPath = fs.PathDir(parentPath) {
for parentPath := fs.PathDir(destDir); ; parentPath = fs.PathDir(parentPath) {
parentPaths = append(parentPaths, parentPath)
if fs.PathDir(parentPath) == parentPath {
break
}
_, err := fs.Stat(parentPath)
if err == nil {
// Exit loop at the closest existing ancestor.
foundExistingAncestor = true
break
}
if !oserror.IsNotExist(err) {
return nil, err
}
}
// Handle empty filesystem edge case.
if !foundExistingAncestor {
parentPaths = append(parentPaths, "")
}
// Create destDir and any of its missing parents.
if err := fs.MkdirAll(destDir, 0755); err != nil {
return nil, err
@@ -142,11 +181,14 @@ func (d *DB) Checkpoint(
// file number.
memQueue := d.mu.mem.queue
current := d.mu.versions.currentVersion()
formatVers := d.mu.formatVers.vers
formatVers := d.FormatMajorVersion()
manifestFileNum := d.mu.versions.manifestFileNum
manifestSize := d.mu.versions.manifest.Size()
optionsFileNum := d.optionsFileNum
virtualBackingFiles := make(map[base.DiskFileNum]struct{})
for diskFileNum := range d.mu.versions.backingState.fileBackingMap {
virtualBackingFiles[diskFileNum] = struct{}{}
}
// Release the manifest and DB.mu so we don't block other operations on
// the database.
d.mu.versions.logUnlock()
@@ -154,13 +196,10 @@ func (d *DB) Checkpoint(
// Wrap the normal filesystem with one which wraps newly created files with
// vfs.NewSyncingFile.
fs := syncingFS{
FS: d.opts.FS,
syncOpts: vfs.SyncingFileOptions{
NoSyncOnClose: d.opts.NoSyncOnClose,
BytesPerSync: d.opts.BytesPerSync,
},
}
fs := vfs.NewSyncingFS(d.opts.FS, vfs.SyncingFileOptions{
NoSyncOnClose: d.opts.NoSyncOnClose,
BytesPerSync: d.opts.BytesPerSync,
})
// Create the dir and its parents (if necessary), and sync them.
var dir vfs.File
@@ -170,11 +209,7 @@ func (d *DB) Checkpoint(
}
if ckErr != nil {
// Attempt to cleanup on error.
paths, _ := fs.List(destDir)
for _, path := range paths {
_ = fs.Remove(path)
}
_ = fs.Remove(destDir)
_ = fs.RemoveAll(destDir)
}
}()
dir, ckErr = mkdirAllAndSyncParents(fs, destDir)
@@ -214,45 +249,34 @@ func (d *DB) Checkpoint(
}
}
{
// Copy the MANIFEST, and create a pointer to it. We copy rather
// than link because additional version edits added to the
// MANIFEST after we took our snapshot of the sstables will
// reference sstables that aren't in our checkpoint. For a
// similar reason, we need to limit how much of the MANIFEST we
// copy.
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeManifest, manifestFileNum)
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
ckErr = vfs.LimitedCopy(fs, srcPath, destPath, manifestSize)
if ckErr != nil {
return ckErr
}
// Recent format versions use an atomic marker for setting the
// active manifest. Older versions use the CURRENT file. The
// setCurrentFunc function will return a closure that will
// take the appropriate action for the database's format
// version.
var manifestMarker *atomicfs.Marker
manifestMarker, _, ckErr = atomicfs.LocateMarker(fs, destDir, manifestMarkerName)
if ckErr != nil {
return ckErr
}
ckErr = setCurrentFunc(formatVers, manifestMarker, fs, destDir, dir)(manifestFileNum)
if ckErr != nil {
return ckErr
}
ckErr = manifestMarker.Close()
if ckErr != nil {
return ckErr
}
}
var excludedFiles map[deletedFileEntry]*fileMetadata
// Set of FileBacking.DiskFileNum which will be required by virtual sstables
// in the checkpoint.
requiredVirtualBackingFiles := make(map[base.DiskFileNum]struct{})
// Link or copy the sstables.
for l := range current.Levels {
iter := current.Levels[l].Iter()
for f := iter.First(); f != nil; f = iter.Next() {
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeTable, f.FileNum)
if excludeFromCheckpoint(f, opt, d.cmp) {
if excludedFiles == nil {
excludedFiles = make(map[deletedFileEntry]*fileMetadata)
}
excludedFiles[deletedFileEntry{
Level: l,
FileNum: f.FileNum,
}] = f
continue
}
fileBacking := f.FileBacking
if f.Virtual {
if _, ok := requiredVirtualBackingFiles[fileBacking.DiskFileNum]; ok {
continue
}
requiredVirtualBackingFiles[fileBacking.DiskFileNum] = struct{}{}
}
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeTable, fileBacking.DiskFileNum)
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
ckErr = vfs.LinkOrCopy(fs, srcPath, destPath)
if ckErr != nil {
@@ -261,6 +285,23 @@ func (d *DB) Checkpoint(
}
}
var removeBackingTables []base.DiskFileNum
for diskFileNum := range virtualBackingFiles {
if _, ok := requiredVirtualBackingFiles[diskFileNum]; !ok {
// The backing sstable associated with fileNum is no longer
// required.
removeBackingTables = append(removeBackingTables, diskFileNum)
}
}
ckErr = d.writeCheckpointManifest(
fs, formatVers, destDir, dir, manifestFileNum.DiskFileNum(), manifestSize,
excludedFiles, removeBackingTables,
)
if ckErr != nil {
return ckErr
}
// Copy the WAL files. We copy rather than link because WAL file recycling
// will cause the WAL files to be reused which would invalidate the
// checkpoint.
@@ -269,7 +310,7 @@ func (d *DB) Checkpoint(
if logNum == 0 {
continue
}
srcPath := base.MakeFilepath(fs, d.walDirname, fileTypeLog, logNum)
srcPath := base.MakeFilepath(fs, d.walDirname, fileTypeLog, logNum.DiskFileNum())
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
ckErr = vfs.Copy(fs, srcPath, destPath)
if ckErr != nil {
@@ -286,3 +327,99 @@ func (d *DB) Checkpoint(
dir = nil
return ckErr
}
func (d *DB) writeCheckpointManifest(
fs vfs.FS,
formatVers FormatMajorVersion,
destDirPath string,
destDir vfs.File,
manifestFileNum base.DiskFileNum,
manifestSize int64,
excludedFiles map[deletedFileEntry]*fileMetadata,
removeBackingTables []base.DiskFileNum,
) error {
// Copy the MANIFEST, and create a pointer to it. We copy rather
// than link because additional version edits added to the
// MANIFEST after we took our snapshot of the sstables will
// reference sstables that aren't in our checkpoint. For a
// similar reason, we need to limit how much of the MANIFEST we
// copy.
// If some files are excluded from the checkpoint, also append a block that
// records those files as deleted.
if err := func() error {
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeManifest, manifestFileNum)
destPath := fs.PathJoin(destDirPath, fs.PathBase(srcPath))
src, err := fs.Open(srcPath, vfs.SequentialReadsOption)
if err != nil {
return err
}
defer src.Close()
dst, err := fs.Create(destPath)
if err != nil {
return err
}
defer dst.Close()
// Copy all existing records. We need to copy at the record level in case we
// need to append another record with the excluded files (we cannot simply
// append a record after a raw data copy; see
// https://github.com/cockroachdb/cockroach/issues/100935).
r := record.NewReader(&io.LimitedReader{R: src, N: manifestSize}, manifestFileNum.FileNum())
w := record.NewWriter(dst)
for {
rr, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
return err
}
rw, err := w.Next()
if err != nil {
return err
}
if _, err := io.Copy(rw, rr); err != nil {
return err
}
}
if len(excludedFiles) > 0 {
// Write out an additional VersionEdit that deletes the excluded SST files.
ve := versionEdit{
DeletedFiles: excludedFiles,
RemovedBackingTables: removeBackingTables,
}
rw, err := w.Next()
if err != nil {
return err
}
if err := ve.Encode(rw); err != nil {
return err
}
}
if err := w.Close(); err != nil {
return err
}
return dst.Sync()
}(); err != nil {
return err
}
// Recent format versions use an atomic marker for setting the
// active manifest. Older versions use the CURRENT file. The
// setCurrentFunc function will return a closure that will
// take the appropriate action for the database's format
// version.
var manifestMarker *atomicfs.Marker
manifestMarker, _, err := atomicfs.LocateMarker(fs, destDirPath, manifestMarkerName)
if err != nil {
return err
}
if err := setCurrentFunc(formatVers, manifestMarker, fs, destDirPath, destDir)(manifestFileNum.FileNum()); err != nil {
return err
}
return manifestMarker.Close()
}

View File

@@ -4,7 +4,18 @@
package pebble
import "github.com/cockroachdb/pebble/internal/base"
import (
"context"
"runtime/pprof"
"sync"
"time"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/objstorage"
"github.com/cockroachdb/tokenbucket"
)
// Cleaner exports the base.Cleaner type.
type Cleaner = base.Cleaner
@@ -14,3 +25,271 @@ type DeleteCleaner = base.DeleteCleaner
// ArchiveCleaner exports the base.ArchiveCleaner type.
type ArchiveCleaner = base.ArchiveCleaner
type cleanupManager struct {
opts *Options
objProvider objstorage.Provider
onTableDeleteFn func(fileSize uint64)
deletePacer *deletionPacer
// jobsCh is used as the cleanup job queue.
jobsCh chan *cleanupJob
// waitGroup is used to wait for the background goroutine to exit.
waitGroup sync.WaitGroup
mu struct {
sync.Mutex
// totalJobs is the total number of enqueued jobs (completed or in progress).
totalJobs int
completedJobs int
completedJobsCond sync.Cond
jobsQueueWarningIssued bool
}
}
// We can queue this many jobs before we have to block EnqueueJob.
const jobsQueueDepth = 1000
// obsoleteFile holds information about a file that needs to be deleted soon.
type obsoleteFile struct {
dir string
fileNum base.DiskFileNum
fileType fileType
fileSize uint64
}
type cleanupJob struct {
jobID int
obsoleteFiles []obsoleteFile
}
// openCleanupManager creates a cleanupManager and starts its background goroutine.
// The cleanupManager must be Close()d.
func openCleanupManager(
opts *Options,
objProvider objstorage.Provider,
onTableDeleteFn func(fileSize uint64),
getDeletePacerInfo func() deletionPacerInfo,
) *cleanupManager {
cm := &cleanupManager{
opts: opts,
objProvider: objProvider,
onTableDeleteFn: onTableDeleteFn,
deletePacer: newDeletionPacer(time.Now(), int64(opts.TargetByteDeletionRate), getDeletePacerInfo),
jobsCh: make(chan *cleanupJob, jobsQueueDepth),
}
cm.mu.completedJobsCond.L = &cm.mu.Mutex
cm.waitGroup.Add(1)
go func() {
pprof.Do(context.Background(), gcLabels, func(context.Context) {
cm.mainLoop()
})
}()
return cm
}
// Close stops the background goroutine, waiting until all queued jobs are completed.
// Delete pacing is disabled for the remaining jobs.
func (cm *cleanupManager) Close() {
close(cm.jobsCh)
cm.waitGroup.Wait()
}
// EnqueueJob adds a cleanup job to the manager's queue.
func (cm *cleanupManager) EnqueueJob(jobID int, obsoleteFiles []obsoleteFile) {
job := &cleanupJob{
jobID: jobID,
obsoleteFiles: obsoleteFiles,
}
// Report deleted bytes to the pacer, which can use this data to potentially
// increase the deletion rate to keep up. We want to do this at enqueue time
// rather than when we get to the job, otherwise the reported bytes will be
// subject to the throttling rate which defeats the purpose.
var pacingBytes uint64
for _, of := range obsoleteFiles {
if cm.needsPacing(of.fileType, of.fileNum) {
pacingBytes += of.fileSize
}
}
if pacingBytes > 0 {
cm.deletePacer.ReportDeletion(time.Now(), pacingBytes)
}
cm.mu.Lock()
cm.mu.totalJobs++
cm.maybeLogLocked()
cm.mu.Unlock()
if invariants.Enabled && len(cm.jobsCh) >= cap(cm.jobsCh)-2 {
panic("cleanup jobs queue full")
}
cm.jobsCh <- job
}
// Wait until the completion of all jobs that were already queued.
//
// Does not wait for jobs that are enqueued during the call.
//
// Note that DB.mu should not be held while calling this method; the background
// goroutine needs to acquire DB.mu to update deleted table metrics.
func (cm *cleanupManager) Wait() {
cm.mu.Lock()
defer cm.mu.Unlock()
n := cm.mu.totalJobs
for cm.mu.completedJobs < n {
cm.mu.completedJobsCond.Wait()
}
}
// mainLoop runs the manager's background goroutine.
func (cm *cleanupManager) mainLoop() {
defer cm.waitGroup.Done()
var tb tokenbucket.TokenBucket
// Use a token bucket with 1 token / second refill rate and 1 token burst.
tb.Init(1.0, 1.0)
for job := range cm.jobsCh {
for _, of := range job.obsoleteFiles {
if of.fileType != fileTypeTable {
path := base.MakeFilepath(cm.opts.FS, of.dir, of.fileType, of.fileNum)
cm.deleteObsoleteFile(of.fileType, job.jobID, path, of.fileNum, of.fileSize)
} else {
cm.maybePace(&tb, of.fileType, of.fileNum, of.fileSize)
cm.onTableDeleteFn(of.fileSize)
cm.deleteObsoleteObject(fileTypeTable, job.jobID, of.fileNum)
}
}
cm.mu.Lock()
cm.mu.completedJobs++
cm.mu.completedJobsCond.Broadcast()
cm.maybeLogLocked()
cm.mu.Unlock()
}
}
func (cm *cleanupManager) needsPacing(fileType base.FileType, fileNum base.DiskFileNum) bool {
if fileType != fileTypeTable {
return false
}
meta, err := cm.objProvider.Lookup(fileType, fileNum)
if err != nil {
// The object was already removed from the provider; we won't actually
// delete anything, so we don't need to pace.
return false
}
// Don't throttle deletion of remote objects.
return !meta.IsRemote()
}
// maybePace sleeps before deleting an object if appropriate. It is always
// called from the background goroutine.
func (cm *cleanupManager) maybePace(
tb *tokenbucket.TokenBucket, fileType base.FileType, fileNum base.DiskFileNum, fileSize uint64,
) {
if !cm.needsPacing(fileType, fileNum) {
return
}
tokens := cm.deletePacer.PacingDelay(time.Now(), fileSize)
if tokens == 0.0 {
// The token bucket might be in debt; it could make us wait even for 0
// tokens. We don't want that if the pacer decided throttling should be
// disabled.
return
}
// Wait for tokens. We use a token bucket instead of sleeping outright because
// the token bucket accumulates up to one second of unused tokens.
for {
ok, d := tb.TryToFulfill(tokenbucket.Tokens(tokens))
if ok {
break
}
time.Sleep(d)
}
}
// deleteObsoleteFile deletes a (non-object) file that is no longer needed.
func (cm *cleanupManager) deleteObsoleteFile(
fileType fileType, jobID int, path string, fileNum base.DiskFileNum, fileSize uint64,
) {
// TODO(peter): need to handle this error, probably by re-adding the
// file that couldn't be deleted to one of the obsolete slices map.
err := cm.opts.Cleaner.Clean(cm.opts.FS, fileType, path)
if oserror.IsNotExist(err) {
return
}
switch fileType {
case fileTypeLog:
cm.opts.EventListener.WALDeleted(WALDeleteInfo{
JobID: jobID,
Path: path,
FileNum: fileNum.FileNum(),
Err: err,
})
case fileTypeManifest:
cm.opts.EventListener.ManifestDeleted(ManifestDeleteInfo{
JobID: jobID,
Path: path,
FileNum: fileNum.FileNum(),
Err: err,
})
case fileTypeTable:
panic("invalid deletion of object file")
}
}
func (cm *cleanupManager) deleteObsoleteObject(
fileType fileType, jobID int, fileNum base.DiskFileNum,
) {
if fileType != fileTypeTable {
panic("not an object")
}
var path string
meta, err := cm.objProvider.Lookup(fileType, fileNum)
if err != nil {
path = "<nil>"
} else {
path = cm.objProvider.Path(meta)
err = cm.objProvider.Remove(fileType, fileNum)
}
if cm.objProvider.IsNotExistError(err) {
return
}
switch fileType {
case fileTypeTable:
cm.opts.EventListener.TableDeleted(TableDeleteInfo{
JobID: jobID,
Path: path,
FileNum: fileNum.FileNum(),
Err: err,
})
}
}
// maybeLogLocked issues a log if the job queue gets 75% full and issues a log
// when the job queue gets back to less than 10% full.
//
// Must be called with cm.mu locked.
func (cm *cleanupManager) maybeLogLocked() {
const highThreshold = jobsQueueDepth * 3 / 4
const lowThreshold = jobsQueueDepth / 10
jobsInQueue := cm.mu.totalJobs - cm.mu.completedJobs
if !cm.mu.jobsQueueWarningIssued && jobsInQueue > highThreshold {
cm.mu.jobsQueueWarningIssued = true
cm.opts.Logger.Infof("cleanup falling behind; job queue has over %d jobs", highThreshold)
}
if cm.mu.jobsQueueWarningIssued && jobsInQueue < lowThreshold {
cm.mu.jobsQueueWarningIssued = false
cm.opts.Logger.Infof("cleanup back to normal; job queue has under %d jobs", lowThreshold)
}
}

View File

@@ -8,7 +8,7 @@ import (
"runtime"
"sync"
"sync/atomic"
"unsafe"
"time"
"github.com/cockroachdb/pebble/record"
)
@@ -32,13 +32,13 @@ type commitQueue struct {
//
// The head index is stored in the most-significant bits so that we can
// atomically add to it and the overflow is harmless.
headTail uint64
headTail atomic.Uint64
// slots is a ring buffer of values stored in this queue. The size must be a
// power of 2. A slot is in use until *both* the tail index has moved beyond
// it and the slot value has been set to nil. The slot value is set to nil
// atomically by the consumer and read atomically by the producer.
slots [record.SyncConcurrency]unsafe.Pointer
slots [record.SyncConcurrency]atomic.Pointer[Batch]
}
const dequeueBits = 32
@@ -57,17 +57,17 @@ func (q *commitQueue) pack(head, tail uint32) uint64 {
}
func (q *commitQueue) enqueue(b *Batch) {
ptrs := atomic.LoadUint64(&q.headTail)
ptrs := q.headTail.Load()
head, tail := q.unpack(ptrs)
if (tail+uint32(len(q.slots)))&(1<<dequeueBits-1) == head {
// Queue is full. This should never be reached because commitPipeline.sem
// Queue is full. This should never be reached because commitPipeline.commitQueueSem
// limits the number of concurrent operations.
panic("pebble: not reached")
}
slot := &q.slots[head&uint32(len(q.slots)-1)]
// Check if the head slot has been released by dequeue.
for atomic.LoadPointer(slot) != nil {
// Check if the head slot has been released by dequeueApplied.
for slot.Load() != nil {
// Another goroutine is still cleaning up the tail, so the queue is
// actually still full. We spin because this should resolve itself
// momentarily.
@@ -75,16 +75,20 @@ func (q *commitQueue) enqueue(b *Batch) {
}
// The head slot is free, so we own it.
atomic.StorePointer(slot, unsafe.Pointer(b))
slot.Store(b)
// Increment head. This passes ownership of slot to dequeue and acts as a
// Increment head. This passes ownership of slot to dequeueApplied and acts as a
// store barrier for writing the slot.
atomic.AddUint64(&q.headTail, 1<<dequeueBits)
q.headTail.Add(1 << dequeueBits)
}
func (q *commitQueue) dequeue() *Batch {
// dequeueApplied removes the earliest enqueued Batch, if it is applied.
//
// Returns nil if the commit queue is empty or the earliest Batch is not yet
// applied.
func (q *commitQueue) dequeueApplied() *Batch {
for {
ptrs := atomic.LoadUint64(&q.headTail)
ptrs := q.headTail.Load()
head, tail := q.unpack(ptrs)
if tail == head {
// Queue is empty.
@@ -92,8 +96,8 @@ func (q *commitQueue) dequeue() *Batch {
}
slot := &q.slots[tail&uint32(len(q.slots)-1)]
b := (*Batch)(atomic.LoadPointer(slot))
if b == nil || atomic.LoadUint32(&b.applied) == 0 {
b := slot.Load()
if b == nil || !b.applied.Load() {
// The batch is not ready to be dequeued, or another goroutine has
// already dequeued it.
return nil
@@ -102,13 +106,13 @@ func (q *commitQueue) dequeue() *Batch {
// Confirm head and tail (for our speculative check above) and increment
// tail. If this succeeds, then we own the slot at tail.
ptrs2 := q.pack(head, tail+1)
if atomic.CompareAndSwapUint64(&q.headTail, ptrs, ptrs2) {
if q.headTail.CompareAndSwap(ptrs, ptrs2) {
// We now own slot.
//
// Tell enqueue that we're done with this slot. Zeroing the slot is also
// important so we don't leave behind references that could keep this object
// live longer than necessary.
atomic.StorePointer(slot, nil)
slot.Store(nil)
// At this point enqueue owns the slot.
return b
}
@@ -121,10 +125,10 @@ func (q *commitQueue) dequeue() *Batch {
type commitEnv struct {
// The next sequence number to give to a batch. Protected by
// commitPipeline.mu.
logSeqNum *uint64
logSeqNum *atomic.Uint64
// The visible sequence number at which reads should be performed. Ratcheted
// upwards atomically as batches are applied to the memtable.
visibleSeqNum *uint64
visibleSeqNum *atomic.Uint64
// Apply the batch to the specified memtable. Called concurrently.
apply func(b *Batch, mem *memTable) error
@@ -140,8 +144,8 @@ type commitEnv struct {
// (contained in a single Batch) atomically to the DB. The steps are
// conceptually:
//
// 1. Write the batch to the WAL and optionally sync the WAL
// 2. Apply the mutations in the batch to the memtable
// 1. Write the batch to the WAL and optionally sync the WAL
// 2. Apply the mutations in the batch to the memtable
//
// These two simple steps are made complicated by the desire for high
// performance. In the absence of concurrency, performance is limited by how
@@ -150,10 +154,10 @@ type commitEnv struct {
// pipeline. Performance under concurrency is the primary concern of the commit
// pipeline, though it also needs to maintain two invariants:
//
// 1. Batches need to be written to the WAL in sequence number order.
// 2. Batches need to be made visible for reads in sequence number order. This
// invariant arises from the use of a single sequence number which
// indicates which mutations are visible.
// 1. Batches need to be written to the WAL in sequence number order.
// 2. Batches need to be made visible for reads in sequence number order. This
// invariant arises from the use of a single sequence number which
// indicates which mutations are visible.
//
// Taking these invariants into account, let's revisit the work the commit
// pipeline needs to perform. Writing the batch to the WAL is necessarily
@@ -173,14 +177,14 @@ type commitEnv struct {
//
// The full outline of the commit pipeline operation is as follows:
//
// with commitPipeline mutex locked:
// assign batch sequence number
// write batch to WAL
// (optionally) add batch to WAL sync list
// apply batch to memtable (concurrently)
// wait for earlier batches to apply
// ratchet read sequence number
// (optionally) wait for the WAL to sync
// with commitPipeline mutex locked:
// assign batch sequence number
// write batch to WAL
// (optionally) add batch to WAL sync list
// apply batch to memtable (concurrently)
// wait for earlier batches to apply
// ratchet read sequence number
// (optionally) wait for the WAL to sync
//
// As soon as a batch has been written to the WAL, the commitPipeline mutex is
// released allowing another batch to write to the WAL. Each commit operation
@@ -217,7 +221,28 @@ type commitPipeline struct {
// Queue of pending batches to commit.
pending commitQueue
env commitEnv
sem chan struct{}
// The commit path has two queues:
// - commitPipeline.pending contains batches whose seqnums have not yet been
// published. It is a lock-free single producer multi consumer queue.
// - LogWriter.flusher.syncQ contains state for batches that have asked for
// a sync. It is a lock-free single producer single consumer queue.
// These lock-free queues have a fixed capacity. And since they are
// lock-free, we cannot do blocking waits when pushing onto these queues, in
// case they are full. Additionally, adding to these queues happens while
// holding commitPipeline.mu, and we don't want to block while holding that
// mutex since it is also needed by other code.
//
// Popping from these queues is independent and for a particular batch can
// occur in either order, though it is more common that popping from the
// commitPipeline.pending will happen first.
//
// Due to these constraints, we reserve a unit of space in each queue before
// acquiring commitPipeline.mu, which also ensures that the push operation
// is guaranteed to have space in the queue. The commitQueueSem and
// logSyncQSem are used for this reservation.
commitQueueSem chan struct{}
logSyncQSem chan struct{}
ingestSem chan struct{}
// The mutex to use for synchronizing access to logSeqNum and serializing
// calls to commitEnv.write().
mu sync.Mutex
@@ -226,23 +251,60 @@ type commitPipeline struct {
func newCommitPipeline(env commitEnv) *commitPipeline {
p := &commitPipeline{
env: env,
// The capacity of both commitQueue.slots and syncQueue.slots is set to
// record.SyncConcurrency, which also determines the value of these
// semaphores. We used to have a single semaphore, which required that the
// capacity of these queues be the same. Now that we have two semaphores,
// the capacity of these queues could be changed to be different. Say half
// of the batches asked to be synced, but syncing took 5x the latency of
// adding to the memtable and publishing. Then syncQueue.slots could be
// sized as 0.5*5 of the commitQueue.slots. We can explore this if we find
// that LogWriterMetrics.SyncQueueLen has high utilization under some
// workloads.
//
// NB: the commit concurrency is one less than SyncConcurrency because we
// have to allow one "slot" for a concurrent WAL rotation which will close
// and sync the WAL.
sem: make(chan struct{}, record.SyncConcurrency-1),
commitQueueSem: make(chan struct{}, record.SyncConcurrency-1),
logSyncQSem: make(chan struct{}, record.SyncConcurrency-1),
ingestSem: make(chan struct{}, 1),
}
return p
}
// directWrite is used to directly write to the WAL. commitPipeline.mu must be
// held while this is called. DB.mu must not be held. directWrite will only
// return once the WAL sync is complete. Note that DirectWrite is a special case
// function which is currently only used when ingesting sstables as a flushable.
// Reason carefully about the correctness argument when calling this function
// from any context.
func (p *commitPipeline) directWrite(b *Batch) error {
var syncWG sync.WaitGroup
var syncErr error
syncWG.Add(1)
p.logSyncQSem <- struct{}{}
_, err := p.env.write(b, &syncWG, &syncErr)
syncWG.Wait()
err = firstError(err, syncErr)
return err
}
// Commit the specified batch, writing it to the WAL, optionally syncing the
// WAL, and applying the batch to the memtable. Upon successful return the
// batch's mutations will be visible for reading.
func (p *commitPipeline) Commit(b *Batch, syncWAL bool) error {
// REQUIRES: noSyncWait => syncWAL
func (p *commitPipeline) Commit(b *Batch, syncWAL bool, noSyncWait bool) error {
if b.Empty() {
return nil
}
p.sem <- struct{}{}
commitStartTime := time.Now()
// Acquire semaphores.
p.commitQueueSem <- struct{}{}
if syncWAL {
p.logSyncQSem <- struct{}{}
}
b.commitStats.SemaphoreWaitDuration = time.Since(commitStartTime)
// Prepare the batch for committing: enqueuing the batch in the pending
// queue, determining the batch sequence number and writing the data to the
@@ -250,27 +312,43 @@ func (p *commitPipeline) Commit(b *Batch, syncWAL bool) error {
//
// NB: We set Batch.commitErr on error so that the batch won't be a candidate
// for reuse. See Batch.release().
mem, err := p.prepare(b, syncWAL)
mem, err := p.prepare(b, syncWAL, noSyncWait)
if err != nil {
b.db = nil // prevent batch reuse on error
// NB: we are not doing <-p.commitQueueSem since the batch is still
// sitting in the pending queue. We should consider fixing this by also
// removing the batch from the pending queue.
return err
}
// Apply the batch to the memtable.
if err := p.env.apply(b, mem); err != nil {
b.db = nil // prevent batch reuse on error
// NB: we are not doing <-p.commitQueueSem since the batch is still
// sitting in the pending queue. We should consider fixing this by also
// removing the batch from the pending queue.
return err
}
// Publish the batch sequence number.
p.publish(b)
<-p.sem
<-p.commitQueueSem
if b.commitErr != nil {
b.db = nil // prevent batch reuse on error
if !noSyncWait {
// Already waited for commit, so look at the error.
if b.commitErr != nil {
b.db = nil // prevent batch reuse on error
err = b.commitErr
}
}
return b.commitErr
// Else noSyncWait. The LogWriter can be concurrently writing to
// b.commitErr. We will read b.commitErr in Batch.SyncWait after the
// LogWriter is done writing.
b.commitStats.TotalDuration = time.Since(commitStartTime)
return err
}
// AllocateSeqNum allocates count sequence numbers, invokes the prepare
@@ -280,7 +358,9 @@ func (p *commitPipeline) Commit(b *Batch, syncWAL bool) error {
// sstable ingestion within the commit pipeline. The prepare callback is
// invoked with commitPipeline.mu held, but note that DB.mu is not held and
// must be locked if necessary.
func (p *commitPipeline) AllocateSeqNum(count int, prepare func(), apply func(seqNum uint64)) {
func (p *commitPipeline) AllocateSeqNum(
count int, prepare func(seqNum uint64), apply func(seqNum uint64),
) {
// This method is similar to Commit and prepare. Be careful about trying to
// share additional code with those methods because Commit and prepare are
// performance critical code paths.
@@ -294,7 +374,7 @@ func (p *commitPipeline) AllocateSeqNum(count int, prepare func(), apply func(se
b.setCount(uint32(count))
b.commit.Add(1)
p.sem <- struct{}{}
p.commitQueueSem <- struct{}{}
p.mu.Lock()
@@ -306,12 +386,12 @@ func (p *commitPipeline) AllocateSeqNum(count int, prepare func(), apply func(se
// Assign the batch a sequence number. Note that we use atomic operations
// here to handle concurrent reads of logSeqNum. commitPipeline.mu provides
// mutual exclusion for other goroutines writing to logSeqNum.
logSeqNum := atomic.AddUint64(p.env.logSeqNum, uint64(count)) - uint64(count)
logSeqNum := p.env.logSeqNum.Add(uint64(count)) - uint64(count)
seqNum := logSeqNum
if seqNum == 0 {
// We can't use the value 0 for the global seqnum during ingestion, because
// 0 indicates no global seqnum. So allocate one more seqnum.
atomic.AddUint64(p.env.logSeqNum, 1)
p.env.logSeqNum.Add(1)
seqNum++
}
b.setSeqNum(seqNum)
@@ -321,7 +401,7 @@ func (p *commitPipeline) AllocateSeqNum(count int, prepare func(), apply func(se
// writes that were sequenced before the ingestion. The spin loop is
// unfortunate, but obviates the need for additional synchronization.
for {
visibleSeqNum := atomic.LoadUint64(p.env.visibleSeqNum)
visibleSeqNum := p.env.visibleSeqNum.Load()
if visibleSeqNum == logSeqNum {
break
}
@@ -331,7 +411,7 @@ func (p *commitPipeline) AllocateSeqNum(count int, prepare func(), apply func(se
// Invoke the prepare callback. Note the lack of error reporting. Even if the
// callback internally fails, the sequence number needs to be published in
// order to allow the commit pipeline to proceed.
prepare()
prepare(b.SeqNum())
p.mu.Unlock()
@@ -341,26 +421,33 @@ func (p *commitPipeline) AllocateSeqNum(count int, prepare func(), apply func(se
// Publish the sequence number.
p.publish(b)
<-p.sem
<-p.commitQueueSem
}
func (p *commitPipeline) prepare(b *Batch, syncWAL bool) (*memTable, error) {
func (p *commitPipeline) prepare(b *Batch, syncWAL bool, noSyncWait bool) (*memTable, error) {
n := uint64(b.Count())
if n == invalidBatchCount {
return nil, ErrInvalidBatch
}
count := 1
if syncWAL {
count++
}
// count represents the waiting needed for publish, and optionally the
// waiting needed for the WAL sync.
b.commit.Add(count)
var syncWG *sync.WaitGroup
var syncErr *error
if syncWAL {
syncWG, syncErr = &b.commit, &b.commitErr
switch {
case !syncWAL:
// Only need to wait for the publish.
b.commit.Add(1)
// Remaining cases represent syncWAL=true.
case noSyncWait:
syncErr = &b.commitErr
syncWG = &b.fsyncWait
// Only need to wait synchronously for the publish. The user will
// (asynchronously) wait on the batch's fsyncWait.
b.commit.Add(1)
b.fsyncWait.Add(1)
case !noSyncWait:
syncErr = &b.commitErr
syncWG = &b.commit
// Must wait for both the publish and the WAL fsync.
b.commit.Add(2)
}
p.mu.Lock()
@@ -373,7 +460,7 @@ func (p *commitPipeline) prepare(b *Batch, syncWAL bool) (*memTable, error) {
// Assign the batch a sequence number. Note that we use atomic operations
// here to handle concurrent reads of logSeqNum. commitPipeline.mu provides
// mutual exclusion for other goroutines writing to logSeqNum.
b.setSeqNum(atomic.AddUint64(p.env.logSeqNum, n) - n)
b.setSeqNum(p.env.logSeqNum.Add(n) - n)
// Write the data to the WAL.
mem, err := p.env.write(b, syncWG, syncErr)
@@ -385,24 +472,26 @@ func (p *commitPipeline) prepare(b *Batch, syncWAL bool) (*memTable, error) {
func (p *commitPipeline) publish(b *Batch) {
// Mark the batch as applied.
atomic.StoreUint32(&b.applied, 1)
b.applied.Store(true)
// Loop dequeuing applied batches from the pending queue. If our batch was
// the head of the pending queue we are guaranteed that either we'll publish
// it or someone else will dequeue and publish it. If our batch is not the
// head of the queue then either we'll dequeue applied batches and reach our
// it or someone else will dequeueApplied and publish it. If our batch is not the
// head of the queue then either we'll dequeueApplied applied batches and reach our
// batch or there is an unapplied batch blocking us. When that unapplied
// batch applies it will go through the same process and publish our batch
// for us.
for {
t := p.pending.dequeue()
t := p.pending.dequeueApplied()
if t == nil {
// Wait for another goroutine to publish us. We might also be waiting for
// the WAL sync to finish.
now := time.Now()
b.commit.Wait()
b.commitStats.CommitWaitDuration += time.Since(now)
break
}
if atomic.LoadUint32(&t.applied) != 1 {
if !t.applied.Load() {
panic("not reached")
}
@@ -411,13 +500,13 @@ func (p *commitPipeline) publish(b *Batch) {
// number for a subsequent batch. That's ok as all we're guaranteeing is
// that the sequence number ratchets up.
for {
curSeqNum := atomic.LoadUint64(p.env.visibleSeqNum)
curSeqNum := p.env.visibleSeqNum.Load()
newSeqNum := t.SeqNum() + uint64(t.Count())
if newSeqNum <= curSeqNum {
// t's sequence number has already been published.
break
}
if atomic.CompareAndSwapUint64(p.env.visibleSeqNum, curSeqNum, newSeqNum) {
if p.env.visibleSeqNum.CompareAndSwap(curSeqNum, newSeqNum) {
// We successfully published t's sequence number.
break
}
@@ -426,18 +515,3 @@ func (p *commitPipeline) publish(b *Batch) {
t.commit.Done()
}
}
// ratchetSeqNum allocates and marks visible all sequence numbers less than
// but excluding `nextSeqNum`.
func (p *commitPipeline) ratchetSeqNum(nextSeqNum uint64) {
p.mu.Lock()
defer p.mu.Unlock()
logSeqNum := atomic.LoadUint64(p.env.logSeqNum)
if logSeqNum >= nextSeqNum {
return
}
count := nextSeqNum - logSeqNum
_ = atomic.AddUint64(p.env.logSeqNum, uint64(count)) - uint64(count)
atomic.StoreUint64(p.env.visibleSeqNum, nextSeqNum)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,40 +14,44 @@ type errorIter struct {
}
// errorIter implements the base.InternalIterator interface.
var _ internalIteratorWithStats = (*errorIter)(nil)
var _ internalIterator = (*errorIter)(nil)
func newErrorIter(err error) *errorIter {
return &errorIter{err: err}
}
func (c *errorIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, []byte) {
return nil, nil
func (c *errorIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) SeekPrefixGE(
prefix, key []byte, flags base.SeekGEFlags,
) (*base.InternalKey, []byte) {
return nil, nil
) (*base.InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, []byte) {
return nil, nil
func (c *errorIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) First() (*InternalKey, []byte) {
return nil, nil
func (c *errorIter) First() (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) Last() (*InternalKey, []byte) {
return nil, nil
func (c *errorIter) Last() (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) Next() (*InternalKey, []byte) {
return nil, nil
func (c *errorIter) Next() (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) Prev() (*InternalKey, []byte) {
return nil, nil
func (c *errorIter) Prev() (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) NextPrefix([]byte) (*InternalKey, base.LazyValue) {
return nil, base.LazyValue{}
}
func (c *errorIter) Error() error {
@@ -63,8 +67,6 @@ func (c *errorIter) String() string {
}
func (c *errorIter) SetBounds(lower, upper []byte) {}
func (c *errorIter) Stats() InternalIteratorStats { return InternalIteratorStats{} }
func (c *errorIter) ResetStats() {}
type errorKeyspanIter struct {
err error

View File

@@ -5,11 +5,15 @@
package pebble
import (
"fmt"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/humanize"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/vfs"
"github.com/cockroachdb/redact"
)
@@ -35,10 +39,11 @@ func formatFileNums(tables []TableInfo) string {
return buf.String()
}
// LevelInfo contains info pertaining to a partificular level.
// LevelInfo contains info pertaining to a particular level.
type LevelInfo struct {
Level int
Tables []TableInfo
Score float64
}
func (i LevelInfo) String() string {
@@ -47,8 +52,11 @@ func (i LevelInfo) String() string {
// SafeFormat implements redact.SafeFormatter.
func (i LevelInfo) SafeFormat(w redact.SafePrinter, _ rune) {
w.Printf("L%d [%s] (%s)", redact.Safe(i.Level), redact.Safe(formatFileNums(i.Tables)),
redact.Safe(humanize.Uint64(tablesTotalSize(i.Tables))))
w.Printf("L%d [%s] (%s) Score=%.2f",
redact.Safe(i.Level),
redact.Safe(formatFileNums(i.Tables)),
redact.Safe(humanize.Bytes.Uint64(tablesTotalSize(i.Tables))),
redact.Safe(i.Score))
}
// CompactionInfo contains the info for a compaction event.
@@ -71,6 +79,27 @@ type CompactionInfo struct {
TotalDuration time.Duration
Done bool
Err error
SingleLevelOverlappingRatio float64
MultiLevelOverlappingRatio float64
// Annotations specifies additional info to appear in a compaction's event log line
Annotations compactionAnnotations
}
type compactionAnnotations []string
// SafeFormat implements redact.SafeFormatter.
func (ca compactionAnnotations) SafeFormat(w redact.SafePrinter, _ rune) {
if len(ca) == 0 {
return
}
for i := range ca {
if i != 0 {
w.Print(" ")
}
w.Printf("%s", redact.SafeString(ca[i]))
}
}
func (i CompactionInfo) String() string {
@@ -86,20 +115,25 @@ func (i CompactionInfo) SafeFormat(w redact.SafePrinter, _ rune) {
}
if !i.Done {
w.Printf("[JOB %d] compacting(%s) ", redact.Safe(i.JobID), redact.SafeString(i.Reason))
w.Print(levelInfos(i.Input))
w.Printf("[JOB %d] compacting(%s) ",
redact.Safe(i.JobID),
redact.SafeString(i.Reason))
w.Printf("%s", i.Annotations)
w.Printf("%s; ", levelInfos(i.Input))
w.Printf("OverlappingRatio: Single %.2f, Multi %.2f", i.SingleLevelOverlappingRatio, i.MultiLevelOverlappingRatio)
return
}
outputSize := tablesTotalSize(i.Output.Tables)
w.Printf("[JOB %d] compacted(%s) ", redact.Safe(i.JobID), redact.SafeString(i.Reason))
w.Printf("%s", i.Annotations)
w.Print(levelInfos(i.Input))
w.Printf(" -> L%d [%s] (%s), in %.1fs (%.1fs total), output rate %s/s",
redact.Safe(i.Output.Level),
redact.Safe(formatFileNums(i.Output.Tables)),
redact.Safe(humanize.Uint64(outputSize)),
redact.Safe(humanize.Bytes.Uint64(outputSize)),
redact.Safe(i.Duration.Seconds()),
redact.Safe(i.TotalDuration.Seconds()),
redact.Safe(humanize.Uint64(uint64(float64(outputSize)/i.Duration.Seconds()))))
redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds()))))
}
type levelInfos []LevelInfo
@@ -115,22 +149,7 @@ func (i levelInfos) SafeFormat(w redact.SafePrinter, _ rune) {
// DiskSlowInfo contains the info for a disk slowness event when writing to a
// file.
type DiskSlowInfo struct {
// Path of file being written to.
Path string
// Duration that has elapsed since this disk operation started.
Duration time.Duration
}
func (i DiskSlowInfo) String() string {
return redact.StringWithoutMarkers(i)
}
// SafeFormat implements redact.SafeFormatter.
func (i DiskSlowInfo) SafeFormat(w redact.SafePrinter, _ rune) {
w.Printf("disk slowness detected: write to file %s has been ongoing for %0.1fs",
i.Path, redact.Safe(i.Duration.Seconds()))
}
type DiskSlowInfo = vfs.DiskSlowInfo
// FlushInfo contains the info for a flush event.
type FlushInfo struct {
@@ -140,6 +159,9 @@ type FlushInfo struct {
Reason string
// Input contains the count of input memtables that were flushed.
Input int
// InputBytes contains the total in-memory size of the memtable(s) that were
// flushed. This size includes skiplist indexing data structures.
InputBytes uint64
// Output contains the ouptut table generated by the flush. The output info
// is empty for the flush begin event.
Output []TableInfo
@@ -149,8 +171,14 @@ type FlushInfo struct {
// TotalDuration is the total wall-time duration of the flush, including
// applying the flush to the database. TotalDuration is always ≥ Duration.
TotalDuration time.Duration
Done bool
Err error
// Ingest is set to true if the flush is handling tables that were added to
// the flushable queue via an ingestion operation.
Ingest bool
// IngestLevels are the output levels for each ingested table in the flush.
// This field is only populated when Ingest is true.
IngestLevels []int
Done bool
Err error
}
func (i FlushInfo) String() string {
@@ -169,20 +197,48 @@ func (i FlushInfo) SafeFormat(w redact.SafePrinter, _ rune) {
plural = ""
}
if !i.Done {
w.Printf("[JOB %d] flushing %d memtable", redact.Safe(i.JobID), redact.Safe(i.Input))
w.SafeString(plural)
w.Printf(" to L0")
w.Printf("[JOB %d] ", redact.Safe(i.JobID))
if !i.Ingest {
w.Printf("flushing %d memtable", redact.Safe(i.Input))
w.SafeString(plural)
w.Printf(" (%s) to L0", redact.Safe(humanize.Bytes.Uint64(i.InputBytes)))
} else {
w.Printf("flushing %d ingested table%s", redact.Safe(i.Input), plural)
}
return
}
outputSize := tablesTotalSize(i.Output)
w.Printf("[JOB %d] flushed %d memtable%s to L0 [%s] (%s), in %.1fs (%.1fs total), output rate %s/s",
redact.Safe(i.JobID), redact.Safe(i.Input), plural,
redact.Safe(formatFileNums(i.Output)),
redact.Safe(humanize.Uint64(outputSize)),
redact.Safe(i.Duration.Seconds()),
redact.Safe(i.TotalDuration.Seconds()),
redact.Safe(humanize.Uint64(uint64(float64(outputSize)/i.Duration.Seconds()))))
if !i.Ingest {
if invariants.Enabled && len(i.IngestLevels) > 0 {
panic(errors.AssertionFailedf("pebble: expected len(IngestedLevels) == 0"))
}
w.Printf("[JOB %d] flushed %d memtable%s (%s) to L0 [%s] (%s), in %.1fs (%.1fs total), output rate %s/s",
redact.Safe(i.JobID), redact.Safe(i.Input), plural,
redact.Safe(humanize.Bytes.Uint64(i.InputBytes)),
redact.Safe(formatFileNums(i.Output)),
redact.Safe(humanize.Bytes.Uint64(outputSize)),
redact.Safe(i.Duration.Seconds()),
redact.Safe(i.TotalDuration.Seconds()),
redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds()))))
} else {
if invariants.Enabled && len(i.IngestLevels) == 0 {
panic(errors.AssertionFailedf("pebble: expected len(IngestedLevels) > 0"))
}
w.Printf("[JOB %d] flushed %d ingested flushable%s",
redact.Safe(i.JobID), redact.Safe(len(i.Output)), plural)
for j, level := range i.IngestLevels {
file := i.Output[j]
if j > 0 {
w.Printf(" +")
}
w.Printf(" L%d:%s (%s)", level, file.FileNum, humanize.Bytes.Uint64(file.Size))
}
w.Printf(" in %.1fs (%.1fs total), output rate %s/s",
redact.Safe(i.Duration.Seconds()),
redact.Safe(i.TotalDuration.Seconds()),
redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds()))))
}
}
// ManifestCreateInfo contains info about a manifest creation event.
@@ -205,7 +261,7 @@ func (i ManifestCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) {
w.Printf("[JOB %d] MANIFEST create error: %s", redact.Safe(i.JobID), i.Err)
return
}
w.Printf("[JOB %d] MANIFEST created %s", redact.Safe(i.JobID), redact.Safe(i.FileNum))
w.Printf("[JOB %d] MANIFEST created %s", redact.Safe(i.JobID), i.FileNum)
}
// ManifestDeleteInfo contains the info for a Manifest deletion event.
@@ -227,7 +283,7 @@ func (i ManifestDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) {
w.Printf("[JOB %d] MANIFEST delete error: %s", redact.Safe(i.JobID), i.Err)
return
}
w.Printf("[JOB %d] MANIFEST deleted %s", redact.Safe(i.JobID), redact.Safe(i.FileNum))
w.Printf("[JOB %d] MANIFEST deleted %s", redact.Safe(i.JobID), i.FileNum)
}
// TableCreateInfo contains the info for a table creation event.
@@ -247,7 +303,7 @@ func (i TableCreateInfo) String() string {
// SafeFormat implements redact.SafeFormatter.
func (i TableCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) {
w.Printf("[JOB %d] %s: sstable created %s",
redact.Safe(i.JobID), redact.Safe(i.Reason), redact.Safe(i.FileNum))
redact.Safe(i.JobID), redact.Safe(i.Reason), i.FileNum)
}
// TableDeleteInfo contains the info for a table deletion event.
@@ -266,10 +322,10 @@ func (i TableDeleteInfo) String() string {
func (i TableDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) {
if i.Err != nil {
w.Printf("[JOB %d] sstable delete error %s: %s",
redact.Safe(i.JobID), redact.Safe(i.FileNum), i.Err)
redact.Safe(i.JobID), i.FileNum, i.Err)
return
}
w.Printf("[JOB %d] sstable deleted %s", redact.Safe(i.JobID), redact.Safe(i.FileNum))
w.Printf("[JOB %d] sstable deleted %s", redact.Safe(i.JobID), i.FileNum)
}
// TableIngestInfo contains the info for a table ingestion event.
@@ -283,7 +339,10 @@ type TableIngestInfo struct {
// GlobalSeqNum is the sequence number that was assigned to all entries in
// the ingested table.
GlobalSeqNum uint64
Err error
// flushable indicates whether the ingested sstable was treated as a
// flushable.
flushable bool
Err error
}
func (i TableIngestInfo) String() string {
@@ -297,14 +356,23 @@ func (i TableIngestInfo) SafeFormat(w redact.SafePrinter, _ rune) {
return
}
w.Printf("[JOB %d] ingested", redact.Safe(i.JobID))
if i.flushable {
w.Printf("[JOB %d] ingested as flushable", redact.Safe(i.JobID))
} else {
w.Printf("[JOB %d] ingested", redact.Safe(i.JobID))
}
for j := range i.Tables {
t := &i.Tables[j]
if j > 0 {
w.Printf(",")
}
w.Printf(" L%d:%s (%s)", redact.Safe(t.Level), redact.Safe(t.FileNum),
redact.Safe(humanize.Uint64(t.Size)))
levelStr := ""
if !i.flushable {
levelStr = fmt.Sprintf("L%d:", t.Level)
}
w.Printf(" %s%s (%s)", redact.Safe(levelStr), t.FileNum,
redact.Safe(humanize.Bytes.Uint64(t.Size)))
}
}
@@ -365,12 +433,12 @@ func (i WALCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) {
}
if i.RecycledFileNum == 0 {
w.Printf("[JOB %d] WAL created %s", redact.Safe(i.JobID), redact.Safe(i.FileNum))
w.Printf("[JOB %d] WAL created %s", redact.Safe(i.JobID), i.FileNum)
return
}
w.Printf("[JOB %d] WAL created %s (recycled %s)",
redact.Safe(i.JobID), redact.Safe(i.FileNum), redact.Safe(i.RecycledFileNum))
redact.Safe(i.JobID), i.FileNum, i.RecycledFileNum)
}
// WALDeleteInfo contains the info for a WAL deletion event.
@@ -392,7 +460,7 @@ func (i WALDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) {
w.Printf("[JOB %d] WAL delete error: %s", redact.Safe(i.JobID), i.Err)
return
}
w.Printf("[JOB %d] WAL deleted %s", redact.Safe(i.JobID), redact.Safe(i.FileNum))
w.Printf("[JOB %d] WAL deleted %s", redact.Safe(i.JobID), i.FileNum)
}
// WriteStallBeginInfo contains the info for a write stall begin event.
@@ -427,9 +495,14 @@ type EventListener struct {
// has been installed.
CompactionEnd func(CompactionInfo)
// DiskSlow is invoked after a disk write operation on a file created
// with a disk health checking vfs.FS (see vfs.DefaultWithDiskHealthChecks)
// is observed to exceed the specified disk slowness threshold duration.
// DiskSlow is invoked after a disk write operation on a file created with a
// disk health checking vfs.FS (see vfs.DefaultWithDiskHealthChecks) is
// observed to exceed the specified disk slowness threshold duration. DiskSlow
// is called on a goroutine that is monitoring slowness/stuckness. The callee
// MUST return without doing any IO, or blocking on anything (like a mutex)
// that is waiting on IO. This is imperative in order to reliably monitor for
// slowness, since if this goroutine gets stuck, the monitoring will stop
// working.
DiskSlow func(DiskSlowInfo)
// FlushBegin is invoked after the inputs to a flush have been determined,

View File

@@ -5,6 +5,10 @@
package pebble
import (
"context"
"fmt"
"sort"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/keyspan"
@@ -12,12 +16,59 @@ import (
"github.com/cockroachdb/pebble/sstable"
)
// NewExternalIter takes an input set of sstable files which may overlap
// arbitrarily and returns an Iterator over the merged contents of the sstables.
// ExternalIterOption provide an interface to specify open-time options to
// NewExternalIter.
type ExternalIterOption interface {
// iterApply is called on the iterator during opening in order to set internal
// parameters.
iterApply(*Iterator)
// readerOptions returns any reader options added by this iter option.
readerOptions() []sstable.ReaderOption
}
type externalIterReaderOptions struct {
opts []sstable.ReaderOption
}
func (e *externalIterReaderOptions) iterApply(iterator *Iterator) {
// Do nothing.
}
func (e *externalIterReaderOptions) readerOptions() []sstable.ReaderOption {
return e.opts
}
// ExternalIterReaderOptions returns an ExternalIterOption that specifies
// sstable.ReaderOptions to be applied on sstable readers in NewExternalIter.
func ExternalIterReaderOptions(opts ...sstable.ReaderOption) ExternalIterOption {
return &externalIterReaderOptions{opts: opts}
}
// ExternalIterForwardOnly is an ExternalIterOption that specifies this iterator
// will only be used for forward positioning operations (First, SeekGE, Next).
// This could enable optimizations that take advantage of this invariant.
// Behaviour when a reverse positioning operation is done on an iterator
// opened with this option is unpredictable, though in most cases it should.
type ExternalIterForwardOnly struct{}
func (e ExternalIterForwardOnly) iterApply(iter *Iterator) {
iter.forwardOnly = true
}
func (e ExternalIterForwardOnly) readerOptions() []sstable.ReaderOption {
return nil
}
// NewExternalIter takes an input 2d array of sstable files which may overlap
// across subarrays but not within a subarray (at least as far as points are
// concerned; range keys are allowed to overlap arbitrarily even within a
// subarray), and returns an Iterator over the merged contents of the sstables.
// Input sstables may contain point keys, range keys, range deletions, etc. The
// input files slice must be sorted in reverse chronological ordering. A key in
// a file at a lower index will shadow a key with an identical user key
// contained within a file at a higher index.
// input files slice must be sorted in reverse chronological ordering. A key in a
// file at a lower index subarray will shadow a key with an identical user key
// contained within a file at a higher index subarray. Each subarray must be
// sorted in internal key order, where lower index files contain keys that sort
// left of files with higher indexes.
//
// Input sstables must only contain keys with the zero sequence number.
//
@@ -27,8 +78,20 @@ import (
func NewExternalIter(
o *Options,
iterOpts *IterOptions,
files []sstable.ReadableFile,
extraReaderOpts ...sstable.ReaderOption,
files [][]sstable.ReadableFile,
extraOpts ...ExternalIterOption,
) (it *Iterator, err error) {
return NewExternalIterWithContext(context.Background(), o, iterOpts, files, extraOpts...)
}
// NewExternalIterWithContext is like NewExternalIter, and additionally
// accepts a context for tracing.
func NewExternalIterWithContext(
ctx context.Context,
o *Options,
iterOpts *IterOptions,
files [][]sstable.ReadableFile,
extraOpts ...ExternalIterOption,
) (it *Iterator, err error) {
if iterOpts != nil {
if err := validateExternalIterOpts(iterOpts); err != nil {
@@ -36,17 +99,32 @@ func NewExternalIter(
}
}
var readers []*sstable.Reader
var readers [][]*sstable.Reader
// Ensure we close all the opened readers if we error out.
defer func() {
if err != nil {
for i := range readers {
_ = readers[i].Close()
for j := range readers[i] {
_ = readers[i][j].Close()
}
}
}
}()
readers, err = openExternalTables(o, files, o.MakeReaderOptions(), extraReaderOpts...)
seqNumOffset := 0
var extraReaderOpts []sstable.ReaderOption
for i := range extraOpts {
extraReaderOpts = append(extraReaderOpts, extraOpts[i].readerOptions()...)
}
for _, levelFiles := range files {
seqNumOffset += len(levelFiles)
}
for _, levelFiles := range files {
var subReaders []*sstable.Reader
seqNumOffset -= len(levelFiles)
subReaders, err = openExternalTables(o, levelFiles, seqNumOffset, o.MakeReaderOptions(), extraReaderOpts...)
readers = append(readers, subReaders)
}
if err != nil {
return nil, err
}
@@ -54,11 +132,10 @@ func NewExternalIter(
buf := iterAllocPool.Get().(*iterAlloc)
dbi := &buf.dbi
*dbi = Iterator{
ctx: ctx,
alloc: buf,
cmp: o.Comparer.Compare,
equal: o.equal(),
merge: o.Merger.Merge,
split: o.Comparer.Split,
comparer: *o.Comparer,
readState: nil,
keyBuf: buf.keyBuf,
prefixOrFullSeekKey: buf.prefixOrFullSeekKey,
@@ -67,7 +144,9 @@ func NewExternalIter(
// Add the readers to the Iterator so that Close closes them, and
// SetOptions can re-construct iterators from them.
externalReaders: readers,
newIters: func(f *manifest.FileMetadata, opts *IterOptions, bytesIterated *uint64) (internalIterator, keyspan.FragmentIterator, error) {
newIters: func(
ctx context.Context, f *manifest.FileMetadata, opts *IterOptions,
internalOpts internalIterOpts) (internalIterator, keyspan.FragmentIterator, error) {
// NB: External iterators are currently constructed without any
// `levelIters`. newIters should never be called. When we support
// organizing multiple non-overlapping files into a single level
@@ -80,9 +159,12 @@ func NewExternalIter(
}
if iterOpts != nil {
dbi.opts = *iterOpts
dbi.saveBounds(iterOpts.LowerBound, iterOpts.UpperBound)
dbi.processBounds(iterOpts.LowerBound, iterOpts.UpperBound)
}
finishInitializingExternal(dbi)
for i := range extraOpts {
extraOpts[i].iterApply(dbi)
}
finishInitializingExternal(ctx, dbi)
return dbi, nil
}
@@ -102,84 +184,372 @@ func validateExternalIterOpts(iterOpts *IterOptions) error {
return nil
}
func finishInitializingExternal(it *Iterator) {
func createExternalPointIter(ctx context.Context, it *Iterator) (internalIterator, error) {
// TODO(jackson): In some instances we could generate fewer levels by using
// L0Sublevels code to organize nonoverlapping files into the same level.
// This would allow us to use levelIters and keep a smaller set of data and
// files in-memory. However, it would also require us to identify the bounds
// of all the files upfront.
mlevels := it.alloc.mlevels[:0]
if !it.opts.pointKeys() {
it.pointIter = emptyIter
} else if it.pointIter == nil {
if len(it.externalReaders) > cap(mlevels) {
mlevels = make([]mergingIterLevel, 0, len(it.externalReaders))
}
for _, r := range it.externalReaders {
return emptyIter, nil
} else if it.pointIter != nil {
return it.pointIter, nil
}
mlevels := it.alloc.mlevels[:0]
if len(it.externalReaders) > cap(mlevels) {
mlevels = make([]mergingIterLevel, 0, len(it.externalReaders))
}
for _, readers := range it.externalReaders {
var combinedIters []internalIterator
for _, r := range readers {
var (
rangeDelIter keyspan.FragmentIterator
pointIter internalIterator
err error
)
pointIter, err = r.NewIter(it.opts.LowerBound, it.opts.UpperBound)
if err == nil {
rangeDelIter, err = r.NewRawRangeDelIter()
}
// We could set hideObsoletePoints=true, since we are reading at
// InternalKeySeqNumMax, but we don't bother since these sstables should
// not have obsolete points (so the performance optimization is
// unnecessary), and we don't want to bother constructing a
// BlockPropertiesFilterer that includes obsoleteKeyBlockPropertyFilter.
pointIter, err = r.NewIterWithBlockPropertyFiltersAndContextEtc(
ctx, it.opts.LowerBound, it.opts.UpperBound, nil, /* BlockPropertiesFilterer */
false /* hideObsoletePoints */, false, /* useFilterBlock */
&it.stats.InternalStats, sstable.TrivialReaderProvider{Reader: r})
if err != nil {
pointIter = &errorIter{err: err}
rangeDelIter = &errorKeyspanIter{err: err}
return nil, err
}
rangeDelIter, err = r.NewRawRangeDelIter()
if err != nil {
return nil, err
}
if rangeDelIter == nil && pointIter != nil && it.forwardOnly {
// TODO(bilal): Consider implementing range key pausing in
// simpleLevelIter so we can reduce mergingIterLevels even more by
// sending all sstable iterators to combinedIters, not just those
// corresponding to sstables without range deletes.
combinedIters = append(combinedIters, pointIter)
continue
}
mlevels = append(mlevels, mergingIterLevel{
iter: base.WrapIterWithStats(pointIter),
iter: pointIter,
rangeDelIter: rangeDelIter,
})
}
it.alloc.merging.init(&it.opts, it.cmp, it.split, mlevels...)
it.alloc.merging.snapshot = base.InternalKeySeqNumMax
it.alloc.merging.elideRangeTombstones = true
it.pointIter = &it.alloc.merging
if len(combinedIters) == 1 {
mlevels = append(mlevels, mergingIterLevel{
iter: combinedIters[0],
})
} else if len(combinedIters) > 1 {
sli := &simpleLevelIter{
cmp: it.cmp,
iters: combinedIters,
}
sli.init(it.opts)
mlevels = append(mlevels, mergingIterLevel{
iter: sli,
rangeDelIter: nil,
})
}
}
if len(mlevels) == 1 && mlevels[0].rangeDelIter == nil {
// Set closePointIterOnce to true. This is because we're bypassing the
// merging iter, which turns Close()s on it idempotent for any child
// iterators. The outer Iterator could call Close() on a point iter twice,
// which sstable iterators do not support (as they release themselves to
// a pool).
it.closePointIterOnce = true
return mlevels[0].iter, nil
}
it.alloc.merging.init(&it.opts, &it.stats.InternalStats, it.comparer.Compare, it.comparer.Split, mlevels...)
it.alloc.merging.snapshot = base.InternalKeySeqNumMax
if len(mlevels) <= cap(it.alloc.levelsPositioned) {
it.alloc.merging.levelsPositioned = it.alloc.levelsPositioned[:len(mlevels)]
}
return &it.alloc.merging, nil
}
func finishInitializingExternal(ctx context.Context, it *Iterator) {
pointIter, err := createExternalPointIter(ctx, it)
if err != nil {
it.pointIter = &errorIter{err: err}
} else {
it.pointIter = pointIter
}
it.iter = it.pointIter
if it.opts.rangeKeys() {
it.rangeKeyMasking.init(it, it.comparer.Compare, it.comparer.Split)
var rangeKeyIters []keyspan.FragmentIterator
if it.rangeKey == nil {
it.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState)
it.rangeKey.init(it.cmp, it.split, &it.opts)
it.rangeKey.rangeKeyIter = it.rangeKey.iterConfig.Init(
it.cmp,
base.InternalKeySeqNumMax,
)
for _, r := range it.externalReaders {
if rki, err := r.NewRawRangeKeyIter(); err != nil {
it.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err})
} else if rki != nil {
it.rangeKey.iterConfig.AddLevel(rki)
// We could take advantage of the lack of overlaps in range keys within
// each slice in it.externalReaders, and generate keyspan.LevelIters
// out of those. However, since range keys are expected to be sparse to
// begin with, the performance gain might not be significant enough to
// warrant it.
//
// TODO(bilal): Explore adding a simpleRangeKeyLevelIter that does not
// operate on FileMetadatas (similar to simpleLevelIter), and implements
// this optimization.
for _, readers := range it.externalReaders {
for _, r := range readers {
if rki, err := r.NewRawRangeKeyIter(); err != nil {
rangeKeyIters = append(rangeKeyIters, &errorKeyspanIter{err: err})
} else if rki != nil {
rangeKeyIters = append(rangeKeyIters, rki)
}
}
}
if len(rangeKeyIters) > 0 {
it.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState)
it.rangeKey.init(it.comparer.Compare, it.comparer.Split, &it.opts)
it.rangeKey.rangeKeyIter = it.rangeKey.iterConfig.Init(
&it.comparer,
base.InternalKeySeqNumMax,
it.opts.LowerBound, it.opts.UpperBound,
&it.hasPrefix, &it.prefixOrFullSeekKey,
false /* internalKeys */, &it.rangeKey.internal,
)
for i := range rangeKeyIters {
it.rangeKey.iterConfig.AddLevel(rangeKeyIters[i])
}
}
}
it.rangeKey.iiter.Init(it.cmp, it.iter, it.rangeKey.rangeKeyIter, it.rangeKey,
it.opts.LowerBound, it.opts.UpperBound)
it.iter = &it.rangeKey.iiter
if it.rangeKey != nil {
it.rangeKey.iiter.Init(&it.comparer, it.iter, it.rangeKey.rangeKeyIter,
keyspan.InterleavingIterOpts{
Mask: &it.rangeKeyMasking,
LowerBound: it.opts.LowerBound,
UpperBound: it.opts.UpperBound,
})
it.iter = &it.rangeKey.iiter
}
}
}
func openExternalTables(
o *Options,
files []sstable.ReadableFile,
seqNumOffset int,
readerOpts sstable.ReaderOptions,
extraReaderOpts ...sstable.ReaderOption,
) (readers []*sstable.Reader, err error) {
readers = make([]*sstable.Reader, 0, len(files))
for i := range files {
r, err := sstable.NewReader(files[i], readerOpts, extraReaderOpts...)
readable, err := sstable.NewSimpleReadable(files[i])
if err != nil {
return readers, err
}
r, err := sstable.NewReader(readable, readerOpts, extraReaderOpts...)
if err != nil {
return readers, err
}
// Use the index of the file in files as the sequence number for all of
// its keys.
r.Properties.GlobalSeqNum = uint64(len(files) - i)
r.Properties.GlobalSeqNum = uint64(len(files) - i + seqNumOffset)
readers = append(readers, r)
}
return readers, err
}
// simpleLevelIter is similar to a levelIter in that it merges the points
// from multiple point iterators that are non-overlapping in the key ranges
// they return. It is only expected to support forward iteration and forward
// regular seeking; reverse iteration and prefix seeking is not supported.
// Intended to be a low-overhead, non-FileMetadata dependent option for
// NewExternalIter. To optimize seeking and forward iteration, it maintains
// two slices of child iterators; one of all iterators, and a subset of it that
// contains just the iterators that contain point keys within the current
// bounds.
//
// Note that this levelIter does not support pausing at file boundaries
// in case of range tombstones in this file that could apply to points outside
// of this file (and outside of this level). This is sufficient for optimizing
// the main use cases of NewExternalIter, however for completeness it would make
// sense to build this pausing functionality in.
type simpleLevelIter struct {
cmp Compare
err error
lowerBound []byte
iters []internalIterator
filtered []internalIterator
firstKeys [][]byte
firstKeysBuf []byte
currentIdx int
}
var _ internalIterator = &simpleLevelIter{}
// init initializes this simpleLevelIter.
func (s *simpleLevelIter) init(opts IterOptions) {
s.currentIdx = 0
s.lowerBound = opts.LowerBound
s.resetFilteredIters()
}
func (s *simpleLevelIter) resetFilteredIters() {
s.filtered = s.filtered[:0]
s.firstKeys = s.firstKeys[:0]
s.firstKeysBuf = s.firstKeysBuf[:0]
s.err = nil
for i := range s.iters {
var iterKey *base.InternalKey
if s.lowerBound != nil {
iterKey, _ = s.iters[i].SeekGE(s.lowerBound, base.SeekGEFlagsNone)
} else {
iterKey, _ = s.iters[i].First()
}
if iterKey != nil {
s.filtered = append(s.filtered, s.iters[i])
bufStart := len(s.firstKeysBuf)
s.firstKeysBuf = append(s.firstKeysBuf, iterKey.UserKey...)
s.firstKeys = append(s.firstKeys, s.firstKeysBuf[bufStart:bufStart+len(iterKey.UserKey)])
} else if err := s.iters[i].Error(); err != nil {
s.err = err
}
}
}
func (s *simpleLevelIter) SeekGE(
key []byte, flags base.SeekGEFlags,
) (*base.InternalKey, base.LazyValue) {
if s.err != nil {
return nil, base.LazyValue{}
}
// Find the first file that is entirely >= key. The file before that could
// contain the key we're looking for.
n := sort.Search(len(s.firstKeys), func(i int) bool {
return s.cmp(key, s.firstKeys[i]) <= 0
})
if n > 0 {
s.currentIdx = n - 1
} else {
s.currentIdx = n
}
if s.currentIdx < len(s.filtered) {
if iterKey, val := s.filtered[s.currentIdx].SeekGE(key, flags); iterKey != nil {
return iterKey, val
}
if err := s.filtered[s.currentIdx].Error(); err != nil {
s.err = err
}
s.currentIdx++
}
return s.skipEmptyFileForward(key, flags)
}
func (s *simpleLevelIter) skipEmptyFileForward(
seekKey []byte, flags base.SeekGEFlags,
) (*base.InternalKey, base.LazyValue) {
var iterKey *base.InternalKey
var val base.LazyValue
for s.currentIdx >= 0 && s.currentIdx < len(s.filtered) && s.err == nil {
if seekKey != nil {
iterKey, val = s.filtered[s.currentIdx].SeekGE(seekKey, flags)
} else if s.lowerBound != nil {
iterKey, val = s.filtered[s.currentIdx].SeekGE(s.lowerBound, flags)
} else {
iterKey, val = s.filtered[s.currentIdx].First()
}
if iterKey != nil {
return iterKey, val
}
if err := s.filtered[s.currentIdx].Error(); err != nil {
s.err = err
}
s.currentIdx++
}
return nil, base.LazyValue{}
}
func (s *simpleLevelIter) SeekPrefixGE(
prefix, key []byte, flags base.SeekGEFlags,
) (*base.InternalKey, base.LazyValue) {
panic("unimplemented")
}
func (s *simpleLevelIter) SeekLT(
key []byte, flags base.SeekLTFlags,
) (*base.InternalKey, base.LazyValue) {
panic("unimplemented")
}
func (s *simpleLevelIter) First() (*base.InternalKey, base.LazyValue) {
if s.err != nil {
return nil, base.LazyValue{}
}
s.currentIdx = 0
return s.skipEmptyFileForward(nil /* seekKey */, base.SeekGEFlagsNone)
}
func (s *simpleLevelIter) Last() (*base.InternalKey, base.LazyValue) {
panic("unimplemented")
}
func (s *simpleLevelIter) Next() (*base.InternalKey, base.LazyValue) {
if s.err != nil {
return nil, base.LazyValue{}
}
if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) {
return nil, base.LazyValue{}
}
if iterKey, val := s.filtered[s.currentIdx].Next(); iterKey != nil {
return iterKey, val
}
s.currentIdx++
return s.skipEmptyFileForward(nil /* seekKey */, base.SeekGEFlagsNone)
}
func (s *simpleLevelIter) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) {
if s.err != nil {
return nil, base.LazyValue{}
}
if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) {
return nil, base.LazyValue{}
}
if iterKey, val := s.filtered[s.currentIdx].NextPrefix(succKey); iterKey != nil {
return iterKey, val
}
s.currentIdx++
return s.skipEmptyFileForward(succKey /* seekKey */, base.SeekGEFlagsNone)
}
func (s *simpleLevelIter) Prev() (*base.InternalKey, base.LazyValue) {
panic("unimplemented")
}
func (s *simpleLevelIter) Error() error {
if s.currentIdx >= 0 && s.currentIdx < len(s.filtered) {
s.err = firstError(s.err, s.filtered[s.currentIdx].Error())
}
return s.err
}
func (s *simpleLevelIter) Close() error {
var err error
for i := range s.iters {
err = firstError(err, s.iters[i].Close())
}
return err
}
func (s *simpleLevelIter) SetBounds(lower, upper []byte) {
s.currentIdx = -1
s.lowerBound = lower
for i := range s.iters {
s.iters[i].SetBounds(lower, upper)
}
s.resetFilteredIters()
}
func (s *simpleLevelIter) String() string {
if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) {
return "simpleLevelIter: current=<nil>"
}
return fmt.Sprintf("simpleLevelIter: current=%s", s.filtered[s.currentIdx])
}
var _ internalIterator = &simpleLevelIter{}

View File

@@ -33,7 +33,7 @@ const (
// NB: This is a low-level routine and typically not what you want to
// use. Newer versions of Pebble running newer format major versions do
// not use the CURRENT file. See setCurrentFunc in version_set.go.
func setCurrentFile(dirname string, fs vfs.FS, fileNum FileNum) error {
func setCurrentFile(dirname string, fs vfs.FS, fileNum base.DiskFileNum) error {
newFilename := base.MakeFilepath(fs, dirname, fileTypeCurrent, fileNum)
oldFilename := base.MakeFilepath(fs, dirname, fileTypeTemp, fileNum)
fs.Remove(oldFilename)

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