├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── addr.go ├── doc.go ├── go.mod ├── go.sum ├── istream.go ├── logging.go ├── metrics.go ├── multipub.go ├── nq_bench_test.go ├── nq_example_test.go ├── nq_payload_test.go ├── nq_stress_test.go ├── nq_test.go ├── ostream.go ├── pool.go ├── pprof_test.go ├── proto.go ├── pub.go ├── ringbuf.go └── sub.go /.gitignore: -------------------------------------------------------------------------------- 1 | golangci-lint 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 nanoQ authors 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | THIS_MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 2 | THIS_MAKEFILE_DIR := $(patsubst %/,%,$(dir $(THIS_MAKEFILE_PATH))) 3 | 4 | 5 | all: build 6 | 7 | build: 8 | go build ./... 9 | 10 | golangci-lint: 11 | GOBIN="$(THIS_MAKEFILE_DIR)" go install github.com/golangci/golangci-lint/cmd/golangci-lint 12 | go mod tidy 13 | 14 | lint: golangci-lint 15 | ./golangci-lint run ./... 16 | 17 | # regex to pick a tests (make test T='TestOnlyThis*') 18 | # match everything by default 19 | T?=.* 20 | test: 21 | go test -run "$(T)" -count=1 -race -timeout 5m ./... 22 | 23 | # regex to pick a benchmarks (make bench B='BenchmarkOnlyThis*') 24 | # match everything by default 25 | B?=.* 26 | bench: 27 | go test -bench "$(B)" -benchmem -run 'noTestsPlease' ./... -v 28 | 29 | clean: 30 | rm -f bin/* 31 | 32 | .PHONY: all build lint test clean 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanoQ — high-performance brokerless Pub/Sub for streaming real-time data 2 | 3 | nanoQ is a very minimalistic (opinionated/limited) Pub/Sub transport library. 4 | 5 | Instant "fire and forget" publish with only best-effort delivery guarantee. 6 | 7 | ## Do I need it? 8 | 9 | For telecommunications, media, IoT, gaming, metrics, clicks, etc.: ***it's okay to loose data*** to get the most up to date messages. 10 | 11 | Brokerless means no central broker server; publishers connect directly to subscribers. 12 | 13 | No open Internet. nanoQ is for the private backend infrastructure networks only. There is no integrity / loop detections and others safety mechanisms. 14 | 15 | Bandwidth vs latency is configurable, but ultimately nanoQ prefers bandwidth — it is designed to carry hundreds and thousands parallel streams through the backend infrastructure. 16 | 17 | ## In a Nutshell 18 | 19 | * tiny: under 1K LOC 20 | * high-performance: no allocations on critical paths, granular locking, and other optimizations 21 | * low overhead simple protocol: 1-5 bytes of metadata (varint length prefix) per message, depending on the message size 22 | * battle-tested: running in production 23 | * data oblivious: JSON, protobuf, thrift, gzip, or XML - all goes 24 | * kubernetes and cloud-native: easy to integrate with existing load-balancers 25 | * scalable: no coordinator or a central server 26 | * simple URL address scheme: `tcp4://localhost:1234` or `unix:///var/run/sockety.sock` 27 | * go native: context, modules and errors wrapping 28 | * transparent: with Prometheus metrics and pluggable logging 29 | 30 | ## Quick start 31 | 32 | ### Subscriber 33 | 34 | ```go 35 | import ( 36 | "context" 37 | "log" 38 | "time" 39 | 40 | "github.com/aigent/nq" 41 | ) 42 | 43 | func main() { 44 | opts := nq.SubOpts{ 45 | KeepaliveTimeout: 5 * time.Second, 46 | Printf: log.Printf, 47 | } 48 | sub := nq.NewSub("tcp4://:1234", opts, nq.NewDefaultMetrics()) 49 | 50 | go func() { 51 | buf := make([]byte, 4096) 52 | for { 53 | if msg, stream, err := sub.Receive(context.TODO(), buf); err != nil { 54 | log.Println("Error while receiving:", err) 55 | continue 56 | } else { 57 | log.Printf("message from stream '%v' is: %s\n", stream, msg) 58 | } 59 | } 60 | }() 61 | if err := sub.Listen(context.TODO()); err != nil { 62 | log.Println("Listen error:", err) 63 | } 64 | } 65 | ``` 66 | 67 | ### Publisher 68 | 69 | ```go 70 | import ( 71 | "context" 72 | "log" 73 | "time" 74 | 75 | "github.com/aigent/nq" 76 | ) 77 | 78 | func main() { 79 | opts := nq.PubOpts{ 80 | KeepaliveTimeout: 5 * time.Second, 81 | ConnectTimeout: 3 * time.Second, 82 | WriteTimeout: 3 * time.Second, 83 | FlushFrequency: 100 * time.Millisecond, 84 | NoDelay: true, 85 | Printf: log.Printf, 86 | } 87 | pub := nq.NewPub("tcp4://localhost:1234", opts, nq.NewDefaultMetrics()) 88 | for { 89 | // Publish the message using 100 connections 90 | for i := 1; i <= 100; i++ { 91 | if err := pub.Publish(context.TODO(), []byte("Hello nanoQ"), i); err != nil { 92 | log.Println("Error while publishing:", err) 93 | } 94 | } 95 | } 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /addr.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | ) 8 | 9 | type addr struct { 10 | net, host string 11 | } 12 | 13 | func (a addr) Network() string { return a.net } 14 | func (a addr) String() string { return a.host } 15 | 16 | // ParseURL parses net.Addr from net://addr URL (ex. "tcp4://localhost:8080" or "unix://:3999" ) 17 | func ParseURL(s string) (net.Addr, error) { 18 | u, err := url.Parse(s) 19 | errorf := func(format string, a ...interface{}) error { 20 | return fmt.Errorf("nanoq: parse %q: "+format, s, a) 21 | } 22 | if err != nil { 23 | return nil, errorf("%w", err) 24 | } 25 | if u.Opaque != "" { 26 | return nil, errorf("unexpected URL opaque %q", u.Opaque) 27 | } 28 | if u.User != nil { 29 | return nil, errorf("unexpected URL user %q", u.User) 30 | } 31 | if u.Path != "" && u.Path != "/" { 32 | return nil, errorf("unexpected URL path %q", u.Path) 33 | } 34 | if u.RawPath != "" && u.RawPath != "/" { 35 | return nil, errorf("unexpected URL raw path %q", u.RawPath) 36 | } 37 | if u.RawQuery != "" { 38 | return nil, errorf("unexpected URL rawquery %q", u.RawQuery) 39 | } 40 | if u.Fragment != "" { 41 | return nil, errorf("unexpected URL fragment %q", u.Fragment) 42 | } 43 | return addr{net: u.Scheme, host: u.Host}, nil 44 | } 45 | 46 | // MustParseURL is a panic flavored ParseURL 47 | func MustParseURL(s string) net.Addr { 48 | addr, err := ParseURL(s) 49 | if err != nil { 50 | panic(err) 51 | } 52 | return addr 53 | } 54 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package nq (nanoQ) is a minimalistic brokerless Pub-Sub message queue for streaming. 2 | // An application that wishes to receive messages should embed nq.Sub, 3 | // and it will bind to a port and read network traffic. 4 | // Another application can send messages to the first one using nq.Pub 5 | // 6 | // nanoQ is designed to lose messages in a scenario when subscribers are inaccessible or 7 | // not able to keep up with the workload. The aim is to transfer audio, video, or clicks, 8 | // where it is important to get fresh data with minimal delay. 9 | package nq 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aigent/nq 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.2 // indirect 7 | github.com/prometheus/client_golang v1.8.0 8 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 6 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 7 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 8 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 9 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 15 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 16 | github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 17 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 18 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 19 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 20 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 21 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 22 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 23 | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 24 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 25 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 26 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 27 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 28 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 29 | github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= 30 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 31 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 32 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 33 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 34 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 35 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 36 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 37 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 38 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 39 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 40 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 41 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 42 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 43 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 45 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 47 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 48 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 49 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 50 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 51 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 52 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 55 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 56 | github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 57 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 58 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 59 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 60 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 61 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 62 | github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 63 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 64 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 65 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 66 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 67 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 68 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 69 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 70 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 71 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 72 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 73 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 74 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 75 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 80 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 81 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 82 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 83 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 84 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 85 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 86 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 87 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 88 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 89 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 90 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 94 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 95 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 96 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 98 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 99 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 100 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 101 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 102 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 103 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 104 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 105 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 106 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 107 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 108 | github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= 109 | github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 110 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 111 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 112 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 113 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 114 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 115 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 116 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 117 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 118 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 119 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 120 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 121 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 122 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 123 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 124 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 125 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 126 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 127 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 128 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 129 | github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= 130 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 131 | github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 132 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 133 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 134 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 135 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 136 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 137 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 138 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 139 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 140 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 141 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 142 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 143 | github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= 144 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 145 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 146 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 147 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 148 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 149 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 150 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 151 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= 152 | github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= 153 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 154 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 155 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 156 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 157 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 158 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 159 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 160 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 161 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 162 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 163 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 164 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 165 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 166 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 167 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 168 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 169 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 170 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 171 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 172 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 173 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 174 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 175 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 176 | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= 177 | github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= 178 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 179 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 180 | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 181 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 182 | github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= 183 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 184 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 185 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 186 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 187 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 188 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 189 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 190 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 191 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 192 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 193 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= 194 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 195 | github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 196 | github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 197 | github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= 198 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 199 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 200 | github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= 201 | github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 202 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 203 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 204 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 205 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 206 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 207 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 208 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 209 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 210 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 211 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 212 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 213 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 214 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 215 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 216 | github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= 217 | github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= 218 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 219 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 220 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 221 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 222 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 223 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 224 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 225 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 226 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 227 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 228 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 229 | github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= 230 | github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= 231 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 232 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 233 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 234 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 235 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 236 | github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= 237 | github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 238 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 239 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 240 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 241 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 242 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 243 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 244 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 245 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 246 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 247 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 248 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 249 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 250 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 251 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 252 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 253 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 254 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 255 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 256 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 257 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 258 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 259 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 260 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 261 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 262 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 263 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 264 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 265 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 266 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 267 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 268 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 269 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= 270 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 271 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 272 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 273 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 274 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 275 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 276 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 277 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 278 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 279 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 280 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 281 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 282 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 283 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 284 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 285 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 286 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 287 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 288 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 289 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 290 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 291 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 292 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 293 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 294 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 295 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 296 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 297 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 298 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 299 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 300 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 301 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 302 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 303 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 304 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 305 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 306 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 307 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 308 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 309 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 310 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 311 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 312 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 313 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 314 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 318 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 319 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 320 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 321 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 322 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 323 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 324 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 325 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 326 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 327 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 328 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88= 340 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 342 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 343 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 344 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 345 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 346 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 347 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 348 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 349 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 350 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 351 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 352 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 353 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 354 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 355 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 356 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 357 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 358 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 359 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 360 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 361 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 362 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 363 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 364 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 365 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 366 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 367 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 368 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 369 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 370 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 371 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 372 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 373 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 374 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 375 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 376 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 377 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 378 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 379 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 380 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 381 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 382 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 383 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 384 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 385 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 386 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 387 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 388 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 389 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 390 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 391 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 392 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 393 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 394 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 395 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 396 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 397 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 398 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 399 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 400 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 401 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 402 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 403 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 404 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 405 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 406 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 407 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 408 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 409 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 410 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 411 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 412 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 413 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 414 | -------------------------------------------------------------------------------- /istream.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/binary" 7 | "io" 8 | "net" 9 | "time" 10 | 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | type istream struct { 15 | r *bufio.Reader 16 | buf []byte 17 | } 18 | 19 | func newIStream() *istream { 20 | return &istream{} 21 | } 22 | 23 | func (is *istream) Work(ctx context.Context, s *sub, streamID uint32, conn net.Conn) error { 24 | m := newIStreamMetrics(conn.RemoteAddr(), s.metrics) 25 | var scratchpad [4]byte // uint32 26 | binary.LittleEndian.PutUint32(scratchpad[:], streamID) 27 | if is.r == nil { 28 | is.r = bufio.NewReader(conn) 29 | } else { 30 | is.r.Reset(conn) 31 | } 32 | 33 | // read loop 34 | for { 35 | if s.KeepaliveTimeout > 0 { 36 | if err := conn.SetReadDeadline(time.Now().Add(s.KeepaliveTimeout)); err != nil { 37 | return err 38 | } 39 | } 40 | 41 | size, err := decodeSize(is.r) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if len(is.buf) < size { 47 | is.buf = make([]byte, size) 48 | } 49 | if _, err = io.ReadFull(is.r, is.buf[:size]); err != nil { 50 | return err 51 | } 52 | // synchronized write to the shared ring buffer 53 | s.enqueue(scratchpad[:], is.buf[:size]) 54 | // update metrics 55 | m.receivedMsgs.Inc() 56 | m.receivedBytes.Add(float64(size)) 57 | 58 | select { 59 | case <-ctx.Done(): 60 | return ctx.Err() 61 | default: 62 | continue 63 | } 64 | } 65 | } 66 | 67 | type istreamMetrics struct { 68 | receivedBytes prometheus.Counter 69 | receivedMsgs prometheus.Counter 70 | } 71 | 72 | func newIStreamMetrics(addr net.Addr, m *Metrics) *istreamMetrics { 73 | var host string 74 | // strip remote port to reduce the amount of labels 75 | if h, _, err := net.SplitHostPort(addr.String()); err == nil { 76 | host = h 77 | } else { 78 | host = addr.String() 79 | } 80 | l := prometheus.Labels{lAddr: host} 81 | return &istreamMetrics{ 82 | receivedBytes: m.receivedBytes.With(l), 83 | receivedMsgs: m.receivedMsgs.With(l), 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type printf = func(f string, v ...interface{}) 8 | 9 | // out is a Printf wrapper 10 | func out(printf printf, f string, v ...interface{}) { 11 | if printf != nil { 12 | printf("nanoq: "+f, v...) 13 | } 14 | } 15 | 16 | // closeConn verbosely close the connection 17 | func closeConn(printf printf, conn net.Conn, direction string) { 18 | if conn == nil { 19 | return 20 | } 21 | m := "close connection" 22 | if addr := conn.RemoteAddr(); addr != nil { 23 | m += " " + direction + " " + addr.String() 24 | } 25 | if err := conn.Close(); err != nil { 26 | m = "failed to " + m + ": " + err.Error() 27 | } 28 | out(printf, m) 29 | } 30 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | // Metrics provide insights to the current state of the app 8 | type Metrics struct { 9 | numStreams *prometheus.GaugeVec 10 | streamDurationSec *prometheus.HistogramVec 11 | receivedBytes *prometheus.CounterVec 12 | receivedMsgs *prometheus.CounterVec 13 | sentBytes *prometheus.CounterVec 14 | sentMsgs *prometheus.CounterVec 15 | lostBytes *prometheus.CounterVec 16 | lostMsgs *prometheus.CounterVec 17 | numReconnects *prometheus.CounterVec 18 | } 19 | 20 | const ( 21 | lAddr = "addr" 22 | ) 23 | 24 | // NewMetrics constructs and registers Metrics 25 | func NewMetrics(reg prometheus.Registerer) *Metrics { 26 | const ns = "nanoq" 27 | l := []string{lAddr} 28 | 29 | metrics := &Metrics{ 30 | receivedBytes: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "received_bytes"}, l), 31 | receivedMsgs: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "received_msgs"}, l), 32 | sentBytes: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "sent_bytes"}, l), 33 | sentMsgs: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "sent_msgs"}, l), 34 | lostBytes: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "lost_bytes"}, l), 35 | lostMsgs: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "lost_msgs"}, l), 36 | numReconnects: prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: ns, Name: "reconnects"}, l), 37 | numStreams: prometheus.NewGaugeVec(prometheus.GaugeOpts{Namespace: ns, Name: "streams"}, l), 38 | streamDurationSec: prometheus.NewHistogramVec(prometheus.HistogramOpts{Namespace: ns, Name: "stream_duration_sec", Buckets: dayOfSecondsBuckets()}, l), 39 | } 40 | reg.MustRegister(metrics.receivedBytes) 41 | reg.MustRegister(metrics.receivedMsgs) 42 | reg.MustRegister(metrics.sentBytes) 43 | reg.MustRegister(metrics.sentMsgs) 44 | reg.MustRegister(metrics.lostBytes) 45 | reg.MustRegister(metrics.lostMsgs) 46 | reg.MustRegister(metrics.numReconnects) 47 | reg.MustRegister(metrics.numStreams) 48 | reg.MustRegister(metrics.streamDurationSec) 49 | return metrics 50 | } 51 | 52 | // NewDefaultMetrics constructs and registers Metrics with the prometheus.DefaultRegisterer 53 | func NewDefaultMetrics() *Metrics { 54 | return NewMetrics(prometheus.DefaultRegisterer) 55 | } 56 | 57 | // dayOfSecondsBuckets for streamDurationSec histogram 58 | func dayOfSecondsBuckets() []float64 { 59 | var ( 60 | unit float64 = 1 61 | buckets []float64 62 | ) 63 | buckets = append(buckets, []float64{1 * unit, 2 * unit, 5 * unit, 10 * unit, 15 * unit, 30 * unit, 45 * unit}...) // sec 64 | unit *= 60 65 | buckets = append(buckets, []float64{1 * unit, 2 * unit, 5 * unit, 10 * unit, 15 * unit, 30 * unit, 45 * unit}...) // min 66 | unit *= 60 67 | buckets = append(buckets, []float64{1 * unit, 2 * unit, 5 * unit, 12 * unit, 18 * unit, 24 * unit}...) // hr 68 | return buckets 69 | } 70 | -------------------------------------------------------------------------------- /multipub.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import "context" 4 | 5 | type multiPub []Pub 6 | 7 | // NewMultiPub constructs a Pub that publishes simultaneously to several destination Subs 8 | func NewMultiPub(urls []string, opts PubOpts, metr *Metrics) Pub { 9 | var mp multiPub 10 | for _, u := range urls { 11 | mp = append(mp, NewPub(u, opts, metr)) 12 | } 13 | return mp 14 | } 15 | 16 | func (mp multiPub) Publish(ctx context.Context, payload []byte, streamID interface{}) error { 17 | var err error 18 | for _, p := range mp { 19 | if err2 := p.Publish(ctx, payload, streamID); err2 != nil && err == nil { 20 | err = err2 21 | } 22 | } 23 | return err 24 | } 25 | -------------------------------------------------------------------------------- /nq_bench_test.go: -------------------------------------------------------------------------------- 1 | package nq_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Benchmark1Stream1s(b *testing.B) { 9 | transferMany(b, 1, time.Second) 10 | } 11 | 12 | func Benchmark1Stream3s(b *testing.B) { 13 | transferMany(b, 1, 3*time.Second) 14 | } 15 | 16 | func Benchmark10Streams3s(b *testing.B) { 17 | transferMany(b, 10, 3*time.Second) 18 | } 19 | 20 | func Benchmark50Streams30s(b *testing.B) { 21 | transferMany(b, 50, 30*time.Second) 22 | } 23 | 24 | func Benchmark100Streams1m(b *testing.B) { 25 | transferMany(b, 100, time.Minute) 26 | } 27 | -------------------------------------------------------------------------------- /nq_example_test.go: -------------------------------------------------------------------------------- 1 | package nq_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/aigent/nq" 9 | ) 10 | 11 | func ExamplePub() { 12 | opts := nq.PubOpts{ 13 | KeepaliveTimeout: 5 * time.Second, 14 | ConnectTimeout: 3 * time.Second, 15 | WriteTimeout: 3 * time.Second, 16 | FlushFrequency: 100 * time.Millisecond, 17 | NoDelay: true, 18 | Printf: log.Printf, 19 | } 20 | pub := nq.NewPub("tcp4://localhost:1234", opts, nq.NewDefaultMetrics()) 21 | for { 22 | // Publish the message using 100 connections 23 | for i := 1; i <= 100; i++ { 24 | _ = pub.Publish(context.TODO(), []byte("Hello nanoQ"), i) 25 | } 26 | } 27 | } 28 | 29 | func ExampleSub() { 30 | opts := nq.SubOpts{ 31 | KeepaliveTimeout: 5 * time.Second, 32 | Printf: log.Printf, 33 | } 34 | sub := nq.NewSub("tcp4://:1234", opts, nq.NewDefaultMetrics()) 35 | 36 | go func() { 37 | buf := make([]byte, maxPayload) 38 | for { 39 | if msg, stream, err := sub.Receive(context.TODO(), buf); err != nil { 40 | log.Println("Error while receiving:", err) 41 | continue 42 | } else { 43 | log.Printf("message from stream '%v' is: %s\n", stream, msg) 44 | } 45 | } 46 | }() 47 | if err := sub.Listen(context.TODO()); err != nil { 48 | log.Println("Listen error:", err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /nq_payload_test.go: -------------------------------------------------------------------------------- 1 | package nq_test 2 | 3 | import ( 4 | "encoding/binary" 5 | "hash/crc64" 6 | "math/rand" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type testMsg []byte 13 | 14 | var crcTable = crc64.MakeTable(crc64.ECMA) 15 | 16 | const ( 17 | timestampLen = 8 // int64 18 | checksumLen = 8 // uint64 19 | minPayload = 80 20 | maxPayload = 250 21 | ) 22 | 23 | func newTestMsg() *testMsg { 24 | length := minPayload + rand.Intn(maxPayload-minPayload) 25 | data := make([]byte, length+timestampLen+checksumLen) 26 | rand.Read(data[timestampLen+checksumLen:]) 27 | binary.LittleEndian.PutUint64(data[timestampLen:], crc64.Checksum(data[timestampLen+checksumLen:], crcTable)) 28 | m := testMsg(data) 29 | m.SetTimestamp() 30 | return &m 31 | } 32 | 33 | var testMsgPool = sync.Pool{New: func() interface{} { return newTestMsg() }} 34 | 35 | func (m *testMsg) Age() time.Duration { 36 | ns := binary.LittleEndian.Uint64(m.Raw()) 37 | return time.Since(time.Unix(0, int64(ns))) 38 | } 39 | 40 | func (m *testMsg) SetTimestamp() { 41 | binary.LittleEndian.PutUint64(m.Raw(), uint64(time.Now().UnixNano())) 42 | } 43 | 44 | func (m *testMsg) Checksum() uint64 { 45 | return binary.LittleEndian.Uint64(m.Raw()[timestampLen:]) 46 | } 47 | 48 | func (m *testMsg) Ok() bool { 49 | data := m.Raw()[timestampLen+checksumLen:] 50 | return crc64.Checksum(data, crcTable) == m.Checksum() 51 | } 52 | 53 | func (m *testMsg) Raw() []byte { return []byte(*m) } 54 | 55 | func TestTestMsgChecksum(t *testing.T) { 56 | p := testMsgPool.Get().(*testMsg) 57 | if !p.Ok() { 58 | t.Fail() 59 | } 60 | testMsgPool.Put(p) 61 | } 62 | -------------------------------------------------------------------------------- /nq_stress_test.go: -------------------------------------------------------------------------------- 1 | package nq_test 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestMultiStreamsIDs(t *testing.T) { 12 | ctx, cancel := context.WithCancel(context.Background()) 13 | urls := []string{"tcp4://localhost:1443"} 14 | pub, subs, wg := newPubSub(ctx, t, urls) 15 | 16 | const maxStreams = 100 17 | const maxTries = 10000000 18 | 19 | streamsIn := make(map[interface{}]int, maxStreams) 20 | streamsOut := make(map[interface{}]int, maxStreams) 21 | 22 | wg.Add(1) 23 | go func() { 24 | defer wg.Done() 25 | for i := 0; i < maxTries; i++ { 26 | streamID := strconv.Itoa(rand.Intn(maxStreams)) 27 | inc := streamsIn[streamID] + 1 28 | if err := pub.Publish(ctx, []byte(strconv.Itoa(inc)), streamID); err != nil { 29 | t.Errorf("error while publishing %v", err) 30 | } 31 | streamsIn[streamID] = inc 32 | } 33 | time.Sleep(5 * time.Second) 34 | cancel() 35 | }() 36 | 37 | wg.Add(1) 38 | go func() { 39 | defer wg.Done() 40 | buf := make([]byte, maxPayload) 41 | payload, streamID, err := subs[0].Receive(ctx, buf) 42 | if err != nil { 43 | return 44 | } 45 | currPlusOne := streamsOut[streamID] + 1 46 | want := strconv.Itoa(currPlusOne) 47 | got := string(payload) 48 | if want != got && want != "1" { 49 | t.Errorf("want=%v, got=%v", want, got) 50 | } 51 | streamsOut[streamID] = currPlusOne 52 | }() 53 | 54 | wg.Wait() 55 | } 56 | -------------------------------------------------------------------------------- /nq_test.go: -------------------------------------------------------------------------------- 1 | package nq_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/aigent/nq" 12 | ) 13 | 14 | var pubOpts = nq.PubOpts{ 15 | //Printf: log.Printf, 16 | } 17 | 18 | var subOpts = nq.SubOpts{ 19 | //Printf: log.Printf, 20 | } 21 | 22 | var metrics = nq.NewDefaultMetrics() 23 | 24 | func newPubSub(ctx context.Context, t testing.TB, urls []string) (nq.Pub, []nq.Sub, *sync.WaitGroup) { 25 | t.Helper() 26 | if len(urls) == 0 { 27 | panic("empty URL list") 28 | } 29 | 30 | // subscriber(s) 31 | var subs []nq.Sub 32 | wg := &sync.WaitGroup{} 33 | for _, url := range urls { 34 | sub := nq.NewSub(url, subOpts, metrics) 35 | wg.Add(1) 36 | go func(url string, sub nq.Sub) { 37 | if err := sub.Listen(ctx); err != context.Canceled { 38 | t.Errorf("Listen %q: %s", url, err) 39 | } 40 | wg.Done() 41 | }(url, sub) 42 | subs = append(subs, sub) 43 | } 44 | 45 | // publisher 46 | var pub nq.Pub 47 | if len(urls) == 1 { 48 | pub = nq.NewPub(urls[0], pubOpts, metrics) 49 | } else { 50 | pub = nq.NewMultiPub(urls, pubOpts, metrics) 51 | } 52 | 53 | return pub, subs, wg 54 | } 55 | 56 | func TestOneMessage(t *testing.T) { 57 | ctx, cancel := context.WithCancel(context.Background()) 58 | pub, subs, wg := newPubSub(ctx, t, []string{"tcp4://localhost:1441"}) 59 | 60 | defer func() { 61 | cancel() 62 | wg.Wait() 63 | }() 64 | 65 | want := []byte("Hello, world") 66 | 67 | // publisher 68 | if err := pub.Publish(ctx, want, 0); err != nil { 69 | t.Error("Failed to Publish:", err) 70 | return 71 | } 72 | 73 | buf := make([]byte, maxPayload) 74 | 75 | have, _, err := subs[0].Receive(ctx, buf) 76 | if err != nil { 77 | if err != context.Canceled { 78 | t.Error(err) 79 | } 80 | return 81 | } 82 | if !bytes.Equal(want, have) { 83 | t.Errorf("%q != %q", string(have), string(want)) 84 | return 85 | } 86 | } 87 | 88 | func TestMultiPub(t *testing.T) { 89 | ctx, cancel := context.WithCancel(context.Background()) 90 | urls := []string{"tcp4://localhost:1443", "tcp4://localhost:1444"} 91 | pub, subs, wg := newPubSub(ctx, t, urls) 92 | 93 | defer func() { 94 | cancel() 95 | wg.Wait() 96 | }() 97 | 98 | msg := []byte("Hello, world") 99 | 100 | // publisher 101 | if err := pub.Publish(ctx, msg, 0); err != nil { 102 | t.Errorf("error while publishing %v", err) 103 | return 104 | } 105 | 106 | buf := make([]byte, maxPayload) 107 | for i := range urls { 108 | payload, _, err := subs[i].Receive(ctx, buf) 109 | if err != nil { 110 | if err != context.Canceled { 111 | t.Error(err) 112 | } 113 | return 114 | } 115 | if !bytes.Equal(payload, msg) { 116 | t.Fail() 117 | } 118 | } 119 | } 120 | 121 | func TestManyMessages(t *testing.T) { 122 | transferMany(t, 1, 3*time.Second) 123 | } 124 | 125 | // transferMany parallel streams. Each stream publishes messages during the `span` perion. 126 | func transferMany(t testing.TB, streams int, span time.Duration) { 127 | ctx, cancel := context.WithCancel(context.Background()) 128 | pub, subs, wg := newPubSub(ctx, t, []string{"tcp4://localhost:1442"}) 129 | 130 | go func() { 131 | time.Sleep(span) 132 | cancel() 133 | }() 134 | defer wg.Wait() 135 | 136 | if b, ok := t.(*testing.B); ok { 137 | b.ResetTimer() 138 | } 139 | 140 | var checkMsgIntegrity bool 141 | if _, ok := t.(*testing.T); ok { 142 | checkMsgIntegrity = true 143 | } 144 | 145 | // publisher(s) 146 | start := time.Now() 147 | go func() { 148 | for time.Since(start) < span { 149 | for s := 0; s < streams; s++ { 150 | m := testMsgPool.Get().(*testMsg) 151 | m.SetTimestamp() 152 | if err := pub.Publish(ctx, m.Raw(), s); err != nil { 153 | _ = err 154 | } 155 | testMsgPool.Put(m) 156 | } 157 | } 158 | }() 159 | 160 | var ( 161 | numRcv int 162 | totalSize int64 163 | 164 | maxLatency, sumLatency time.Duration 165 | minLatency = 99999 * time.Hour 166 | numMeasurements int 167 | buf = make([]byte, maxPayload*2) 168 | ) 169 | 170 | for { 171 | payload, _, err := subs[0].Receive(ctx, buf) 172 | if err != nil { 173 | if err == io.EOF { 174 | continue 175 | } 176 | if err != context.Canceled { 177 | t.Errorf("Receive: %s", err) 178 | } 179 | break 180 | } 181 | var m testMsg = testMsg(payload) 182 | 183 | latency := m.Age() 184 | 185 | if latency > maxLatency { 186 | maxLatency = latency 187 | } 188 | if latency < minLatency { 189 | minLatency = latency 190 | } 191 | sumLatency += latency 192 | numMeasurements++ 193 | 194 | if checkMsgIntegrity && !m.Ok() { 195 | t.Error("Invalid message received") 196 | break 197 | } 198 | totalSize += int64(len(m.Raw())) 199 | numRcv++ 200 | } 201 | 202 | if b, ok := t.(*testing.B); ok { 203 | b.StopTimer() 204 | b.SetBytes(totalSize) 205 | b.ReportMetric(float64(minLatency.Microseconds())/1000.0, "min_ms") 206 | b.ReportMetric(float64(maxLatency.Microseconds())/1000.0, "max_ms") 207 | if numMeasurements == 0 { 208 | numMeasurements = 1 209 | } 210 | b.ReportMetric(float64(sumLatency.Microseconds())/float64(numMeasurements)/1000, "avg_ms") 211 | b.ReportMetric(float64(totalSize)/1024/1024/time.Since(start).Seconds(), "MB/s") 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /ostream.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "net" 9 | "sync" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | // ostream is an output Pub's stream with asynchronous send loop 15 | type ostream struct { 16 | *PubOpts 17 | addr net.Addr 18 | mu sync.Mutex 19 | 20 | e encoder 21 | 22 | frontBuf *ringBuf // used by callers 23 | backBuf *ringBuf // used by async send loop 24 | 25 | // metrics 26 | *pubMetrics 27 | bufferedMsgs float64 28 | bufferedBytes float64 29 | } 30 | 31 | // newOStream constructor 32 | func newOStream(addr net.Addr, opts *PubOpts, p *pubMetrics) *ostream { 33 | os := &ostream{ 34 | PubOpts: opts, 35 | addr: addr, 36 | frontBuf: &ringBuf{}, 37 | backBuf: &ringBuf{}, 38 | pubMetrics: p, 39 | } 40 | 41 | return os 42 | } 43 | 44 | // Post a message into the outgoing queue. 45 | func (os *ostream) Post(b []byte) error { 46 | os.mu.Lock() 47 | defer os.mu.Unlock() 48 | 49 | if err := os.e.EncodeTo(os.frontBuf, b); err != nil { 50 | out(os.Printf, "failed to write: %s", err) 51 | os.lostMsgs.Inc() 52 | os.lostBytes.Add(float64(len(b))) 53 | return err 54 | } 55 | os.bufferedMsgs++ 56 | os.bufferedBytes += float64(len(b)) 57 | 58 | // Do not let send buffer to grow indefinitely 59 | for os.frontBuf.Len() > MaxBuffer { 60 | size, err := decodeSize(os.frontBuf) 61 | if err != nil { 62 | panic("nanoq: failed to decode payload size from the send buffer: " + err.Error()) 63 | } 64 | if err := os.frontBuf.Discard(size); err != nil { 65 | panic("nanoq: failed to discard bytes from the send buffer: " + err.Error()) 66 | } 67 | os.bufferedMsgs-- 68 | os.bufferedBytes -= float64(size) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // Work runs the background send loop. 75 | // It blocks execution unil either ctx is done, no messages after KeepaliveTimeout published, or an unrecoverable error occurs. 76 | func (os *ostream) Work(ctx context.Context) error { 77 | var ( 78 | conn net.Conn 79 | err error 80 | gotMsgAt = time.Now() 81 | flushTick = time.NewTicker(os.FlushFrequency) 82 | ) 83 | 84 | defer func() { 85 | closeConn(os.Printf, conn, "to") 86 | flushTick.Stop() 87 | }() 88 | 89 | // (re-)connect loop 90 | for { 91 | if conn, err = os.dial(ctx); err != nil { 92 | return fmt.Errorf("nanoq: failed to dial %q: %w", os.addr.String(), err) 93 | } 94 | out(os.Printf, "connected to %s", conn.RemoteAddr().String()) 95 | RETRY: 96 | // send loop 97 | for { 98 | select { 99 | case <-flushTick.C: 100 | var ( 101 | bufferedBytes float64 102 | bufferedMsgs float64 103 | ) 104 | 105 | // flip front and back buffers 106 | os.backBuf.Reset() 107 | os.mu.Lock() 108 | os.frontBuf, os.backBuf = os.backBuf, os.frontBuf 109 | bufferedBytes = os.bufferedBytes 110 | os.bufferedBytes = 0 111 | bufferedMsgs = os.bufferedMsgs 112 | os.bufferedMsgs = 0 113 | os.mu.Unlock() 114 | 115 | if os.backBuf.Len() == 0 { 116 | // no new messages 117 | if time.Since(gotMsgAt) > os.KeepaliveTimeout { 118 | return nil 119 | } 120 | continue 121 | } 122 | 123 | gotMsgAt = time.Now() 124 | 125 | // flush write buffer 126 | if err := func() error { 127 | if os.WriteTimeout > 0 { 128 | if err := conn.SetWriteDeadline(time.Now().Add(os.WriteTimeout)); err != nil { 129 | return err 130 | } 131 | } 132 | _, err := os.backBuf.WriteTo(conn) 133 | return err 134 | }(); err != nil { 135 | out(os.Printf, "failed to send: %s", err) 136 | os.lostMsgs.Add(bufferedMsgs) 137 | os.lostBytes.Add(bufferedBytes) 138 | if temporary(err) { 139 | closeConn(os.Printf, conn, "to") 140 | break RETRY 141 | } 142 | return err 143 | } 144 | os.sentMsgs.Add(bufferedMsgs) 145 | os.sentBytes.Add(bufferedBytes) 146 | case <-ctx.Done(): 147 | return ctx.Err() 148 | } 149 | } 150 | os.numReconnects.Inc() 151 | } 152 | } 153 | 154 | func (os *ostream) dial(ctx context.Context) (conn net.Conn, err error) { 155 | var ( 156 | start = time.Now() 157 | delay time.Duration 158 | ) 159 | for elapsed := time.Duration(0); elapsed < os.ConnectTimeout; elapsed = time.Since(start) { 160 | netDialer := net.Dialer{Timeout: os.ConnectTimeout - elapsed} 161 | 162 | if conn, err = netDialer.DialContext(ctx, os.addr.Network(), os.addr.String()); err != nil { 163 | if temporary(err) { 164 | if delay = increaseDelay(delay, os.ConnectTimeout-elapsed); delay > 0 { 165 | // sleep before retry 166 | select { 167 | case <-time.After(delay): 168 | continue 169 | case <-ctx.Done(): 170 | return nil, ctx.Err() 171 | } 172 | } 173 | } 174 | return nil, err 175 | } 176 | if tcp, ok := conn.(*net.TCPConn); ok { 177 | if err := tcp.SetNoDelay(os.NoDelay); err != nil { 178 | return nil, err 179 | } 180 | } 181 | return conn, nil 182 | } 183 | return nil, err 184 | } 185 | 186 | func (os *ostream) Reset() { 187 | os.mu.Lock() 188 | defer os.mu.Unlock() 189 | os.frontBuf.Reset() 190 | os.backBuf.Reset() 191 | os.bufferedMsgs = 0 192 | os.bufferedBytes = 0 193 | } 194 | 195 | // increaseDelay to not spam reconnects 196 | func increaseDelay(d time.Duration, max time.Duration) time.Duration { 197 | const ( 198 | min = time.Millisecond 199 | minTimes = 2 200 | maxTimes = 10 201 | ) 202 | if d < min { 203 | return min 204 | } 205 | // randomize delay to prevent accidental DDoS 206 | d *= time.Duration((minTimes + rand.Intn(maxTimes-minTimes))) 207 | if d > max { 208 | d = max 209 | } 210 | return d 211 | } 212 | 213 | // temporary errors can be retried 214 | func temporary(err error) bool { 215 | if errors.Is(err, syscall.ECONNREFUSED) { 216 | // we assume connection was refused because the subscriber is restarting or temporarily unavailable 217 | return true 218 | } 219 | if err, ok := err.(interface{ Temporary() bool }); ok { 220 | return err.Temporary() 221 | } 222 | return false 223 | } 224 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import "sync" 4 | 5 | // sync.Pool cleans itself too aggressively: every 2nd GC cycle or so 6 | // This is very simple pool with long living items 7 | type pool struct { 8 | items []interface{} 9 | mu sync.Mutex 10 | New func() interface{} 11 | } 12 | 13 | // Get returns one of the available items or try to creates new one 14 | func (p *pool) Get() interface{} { 15 | var item interface{} 16 | 17 | p.mu.Lock() 18 | if len(p.items) > 0 { 19 | last := len(p.items) - 1 20 | item = p.items[last] 21 | p.items = p.items[:last] 22 | } 23 | p.mu.Unlock() 24 | 25 | if item != nil { 26 | return item 27 | } 28 | if p.New != nil { 29 | return p.New() 30 | } 31 | return nil 32 | } 33 | 34 | // Put adds an element to the pool for later reuse 35 | func (p *pool) Put(item interface{}) { 36 | p.mu.Lock() 37 | p.items = append(p.items, item) 38 | p.mu.Unlock() 39 | } 40 | -------------------------------------------------------------------------------- /pprof_test.go: -------------------------------------------------------------------------------- 1 | package nq_test 2 | 3 | import ( 4 | "net/http" 5 | _ "net/http/pprof" 6 | 7 | "log" 8 | ) 9 | 10 | func init() { 11 | go func() { 12 | if err := http.ListenAndServe(":6060", nil); err != http.ErrServerClosed { 13 | log.Printf("Failed to start diagnostic HTTP server: %s\n", err) 14 | } 15 | }() 16 | } 17 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | // Binary protocol 10 | // ~~~~~~~~~~~~~~~ 11 | // 12 | // +=======...=+===============... =+ 13 | // | size ... | payload ... | 14 | // |UVarInt... | (size) octets ... | 15 | // +=======...=+===============... =+ 16 | // | | | 17 | // |<-[1..10]->|<- (size) ...->| 18 | // 19 | // * size of the payload -- unsigned 64bit varint, [1..10] octets. Practically, size is limited by the MaxPayload value. 20 | // * payload: arbitrary bytes, [0 .. MaxPayload] bytes. 21 | 22 | var ( 23 | // MaxPayload prevents receiving side from DoS atack due to allocating too much RAM 24 | MaxPayload = 10 * 1024 * 1024 // 10Mb ought to be enough for anybody 25 | 26 | // ErrTooBig indicates that payload exceeds the limit 27 | ErrTooBig = errors.New("nanoq: payload exceeds the limit") 28 | ) 29 | 30 | const maxHeaderLen = binary.MaxVarintLen64 31 | 32 | type encoder struct { 33 | scratchpad [maxHeaderLen]byte 34 | } 35 | 36 | func (e *encoder) EncodeTo(w io.Writer, p []byte) error { 37 | size := len(p) 38 | if size > MaxPayload { 39 | return ErrTooBig 40 | } 41 | // write size header 42 | n := binary.PutUvarint(e.scratchpad[:], uint64(size)) 43 | if _, err := w.Write(e.scratchpad[:n]); err != nil { 44 | return err 45 | } 46 | // write payload 47 | if _, err := w.Write(p); err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | // decodeSize returns the size of a payload in bytes 54 | func decodeSize(r io.ByteReader) (int, error) { 55 | // header: payload size 56 | size, err := binary.ReadUvarint(r) 57 | if err != nil { 58 | return 0, err 59 | } 60 | if size > uint64(MaxPayload) { 61 | return 0, ErrTooBig 62 | } 63 | return int(size), nil 64 | } 65 | -------------------------------------------------------------------------------- /pub.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | // MaxBuffer value limits internal buffers size to prevent memory blow up 13 | // when there is no connection or network is too slow 14 | var MaxBuffer = MaxPayload * 10 15 | 16 | // PubOpts are publisher's options to tweak 17 | type PubOpts struct { 18 | KeepaliveTimeout time.Duration // For how long to keep a stream alive if there are no new messages (default timeout is used on zero value) 19 | ConnectTimeout time.Duration // Limit (re)-connects time (default timeout is used on zero value) 20 | WriteTimeout time.Duration // Limit socket write operation time (zero means no deadline) 21 | FlushFrequency time.Duration // Socket write frequency (default frequency is used on zero value) 22 | NoDelay bool // Disable or enable Nagle's algorithm (TCP only) 23 | 24 | // Printf optionally specifies a function used for logging 25 | Printf func(format string, v ...interface{}) 26 | } 27 | 28 | // Pub is an asynchronous message publisher. 29 | // 30 | // Publish stores the payload into memory buffer and returns immediately. There is only best effort delivery guarantee, 31 | // nil error only means that message was enqueued for sending. 32 | // The streamKey ties the payload with a certain stream (connection). Each time Publish receives new streamKey it spawns new connection. 33 | // Messages published using the same streamKey will be transferred over the same stream in the same order. 34 | // Note that streamKey exists only on the publisher's side. 35 | // Subscriber provides a key to differentiate streams but it doesn't match with the one passed to the Pub. 36 | type Pub interface { 37 | Publish(ctx context.Context, payload []byte, streamKey interface{}) error 38 | } 39 | 40 | type pub struct { 41 | PubOpts 42 | pubMetrics 43 | addr net.Addr 44 | streamByKey sync.Map 45 | pool pool 46 | } 47 | 48 | const ( 49 | defaultTimeout = 5 * time.Second 50 | defaultFlushFrequency = 50 * time.Millisecond 51 | ) 52 | 53 | // NewPub constructs a single destination Pub 54 | func NewPub(url string, opts PubOpts, m *Metrics) Pub { 55 | addr := MustParseURL(url) 56 | l := prometheus.Labels{lAddr: addr.String()} 57 | 58 | if opts.KeepaliveTimeout == 0 { 59 | opts.KeepaliveTimeout = defaultTimeout 60 | } 61 | if opts.ConnectTimeout == 0 { 62 | opts.ConnectTimeout = defaultTimeout 63 | } 64 | if opts.FlushFrequency == 0 { 65 | opts.FlushFrequency = defaultFlushFrequency 66 | } 67 | 68 | pm := pubMetrics{ 69 | sentBytes: m.sentBytes.With(l), 70 | sentMsgs: m.sentMsgs.With(l), 71 | lostBytes: m.lostBytes.With(l), 72 | lostMsgs: m.lostMsgs.With(l), 73 | numReconnects: m.numReconnects.With(l), 74 | numStreams: m.numStreams.With(l), 75 | streamDurationSec: m.streamDurationSec.With(l), 76 | } 77 | 78 | return &pub{ 79 | PubOpts: opts, 80 | pubMetrics: pm, 81 | addr: addr, 82 | streamByKey: sync.Map{}, 83 | pool: pool{New: func() interface{} { return newOStream(addr, &opts, &pm) }}, 84 | } 85 | } 86 | 87 | func (p *pub) Publish(ctx context.Context, payload []byte, streamKey interface{}) error { 88 | // pick connection by streamKey or create new one 89 | var s *ostream 90 | 91 | // before calling LoadOrStore we want to optimize for the case when ostream exists 92 | if val, ok := p.streamByKey.Load(streamKey); ok { 93 | s = val.(*ostream) 94 | } else if val, loaded := p.streamByKey.LoadOrStore(streamKey, p.pool.Get()); loaded { 95 | // someone has just created ostream 96 | s = val.(*ostream) 97 | } else { 98 | // create new ostream 99 | s = val.(*ostream) 100 | p.numStreams.Inc() 101 | go func() { 102 | start := time.Now() 103 | if err := s.Work(ctx); err != nil { 104 | out(p.Printf, err.Error()) 105 | } 106 | p.streamByKey.Delete(streamKey) 107 | p.streamDurationSec.Observe(time.Since(start).Seconds()) 108 | p.numStreams.Dec() 109 | // reuse ostream 110 | s.Reset() 111 | p.pool.Put(s) 112 | }() 113 | } 114 | 115 | // post to send queue 116 | return s.Post(payload) 117 | } 118 | 119 | type pubMetrics struct { 120 | sentBytes prometheus.Counter 121 | sentMsgs prometheus.Counter 122 | lostBytes prometheus.Counter 123 | lostMsgs prometheus.Counter 124 | numReconnects prometheus.Counter 125 | numStreams prometheus.Gauge 126 | streamDurationSec prometheus.Observer 127 | } 128 | -------------------------------------------------------------------------------- /ringbuf.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // ringBuf is a ReadWriter with underlying memory arena 9 | // when write position reaches the buffer capacity, it overflows and starts from 0 10 | type ringBuf struct { 11 | buf []byte 12 | r, w int 13 | 14 | hasData bool 15 | } 16 | 17 | // used in grow 18 | const minSize = 256 19 | 20 | // read the previously written bytes in two parts to account for a write wraparound 21 | func (b *ringBuf) read() ([]byte, []byte) { 22 | if !b.hasData { 23 | return nil, nil 24 | } 25 | 26 | if b.w > b.r { 27 | // data is linear 28 | return b.buf[b.r:b.w], nil 29 | } 30 | 31 | // data wraps around zero 32 | return b.buf[b.r:], b.buf[:b.w] 33 | } 34 | 35 | // advanceRead moves the read pointer 36 | func (b *ringBuf) advanceRead(n int) { 37 | if n > b.Len() || n < 0 { 38 | panic(fmt.Sprintf("nanoq.RingBuffer: attempt to read %d bytes from %d available", n, b.Len())) 39 | } 40 | if n == 0 { 41 | // return to avoid possible division by zero in the next line 42 | return 43 | } 44 | b.r = (b.r + n) % cap(b.buf) 45 | if b.r == b.w { 46 | b.hasData = false 47 | } 48 | } 49 | 50 | // Read the next len(p) bytes from the buffer or until no data is left. 51 | // It returns the number of bytes read or io.EOF error if no data is available. 52 | func (b *ringBuf) Read(p []byte) (n int, err error) { 53 | p1, p2 := b.read() 54 | if len(p1)+len(p2) == 0 { 55 | // empty 56 | if len(p) == 0 { 57 | return 0, nil 58 | } 59 | return 0, io.EOF 60 | } 61 | 62 | n = copy(p, p1) 63 | n += copy(p[n:], p2) 64 | b.advanceRead(n) 65 | return n, nil 66 | } 67 | 68 | // ReadByte makes ringBuf io.ByteReader compatible. 69 | // It returns io.EOF error if there is no byte available for reading. 70 | func (b *ringBuf) ReadByte() (byte, error) { 71 | p1, p2 := b.read() 72 | if len(p1)+len(p2) == 0 { 73 | return 0, io.EOF 74 | } 75 | var bt byte 76 | if len(p1) > 0 { 77 | bt = p1[0] 78 | } else { 79 | bt = p2[0] 80 | } 81 | b.advanceRead(1) 82 | return bt, nil 83 | } 84 | 85 | // Write p into the memory buffer and return the number of bytes written. 86 | // The buffer will grow automatically. 87 | func (b *ringBuf) Write(p []byte) (int, error) { 88 | // ensure we have enough space 89 | if b.Cap()-b.Len() < len(p) { 90 | b.grow(len(p)) 91 | } 92 | 93 | var n int 94 | 95 | if b.w < b.r { 96 | // the unread portion is linear 97 | n = copy(b.buf[b.w:], p) 98 | } else { 99 | // the unread portion wraps around 100 | n = copy(b.buf[b.w:], p) 101 | n += copy(b.buf, p[n:]) 102 | } 103 | b.w = (b.w + n) % cap(b.buf) 104 | b.hasData = true 105 | return n, nil 106 | } 107 | 108 | // WriteTo writes data to w until the buffer is drained or an error occurs. 109 | func (b *ringBuf) WriteTo(w io.Writer) (n int64, err error) { 110 | p1, p2 := b.read() 111 | l := len(p1) + len(p2) 112 | if l == 0 { 113 | return 0, nil 114 | } 115 | var nBytes int64 116 | for _, p := range [][]byte{p1, p2} { 117 | n, err := w.Write(p) 118 | b.advanceRead(n) 119 | nBytes += int64(n) 120 | if err != nil { 121 | return nBytes, err 122 | } 123 | if n != len(p) { 124 | return nBytes, io.ErrShortWrite 125 | } 126 | } 127 | 128 | return nBytes, nil 129 | } 130 | 131 | func (b *ringBuf) grow(n int) { 132 | p1, p2 := b.read() 133 | newCap := len(p1) + len(p2) + n 134 | if newCap < minSize { 135 | newCap = minSize 136 | } 137 | if newCap < b.Cap()*2 { 138 | newCap = b.Cap() * 2 139 | } 140 | 141 | newBuf := make([]byte, newCap) 142 | copy(newBuf, p1) 143 | copy(newBuf[len(p1):], p2) 144 | b.buf = newBuf 145 | b.r = 0 146 | b.w = len(p1) + len(p2) 147 | } 148 | 149 | func (b *ringBuf) Cap() int { 150 | return cap(b.buf) 151 | } 152 | 153 | func (b *ringBuf) Len() int { 154 | p1, p2 := b.read() 155 | return len(p1) + len(p2) 156 | } 157 | 158 | func (b *ringBuf) Reset() { 159 | b.hasData = false 160 | b.r = 0 161 | b.w = 0 162 | } 163 | 164 | func (b *ringBuf) Discard(n int) error { 165 | if n < 0 { 166 | panic("nanoq.*ringBuf.Discard: negative count") 167 | } 168 | p1, p2 := b.read() 169 | if len(p1)+len(p2) < n { 170 | return io.ErrUnexpectedEOF 171 | } 172 | b.advanceRead(n) 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /sub.go: -------------------------------------------------------------------------------- 1 | package nq 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "net" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/prometheus/client_golang/prometheus" 14 | ) 15 | 16 | // StreamDescriptor is used to separate messages from the different incoming streams 17 | type StreamDescriptor uint32 18 | 19 | // Sub is a subscriber. 20 | // 21 | // Listen should be called for Sub to bind port and to start listen for messages. 22 | // It will block until the ctx will signal Done, and will return ctx.Err(). 23 | // 24 | // Receive reads the next available message into the byte slice p. 25 | // The payload returned by Receive alias the same memory region as p. 26 | // If the buffer p is too short to hold the message Receive returns io.ErrShortBuffer and discards the message. 27 | // Receive will block until either new message is available for readind or ctx signals Done. 28 | // The stream descriptor returned by Receive is not the same as streamKey in Publish, it is an arbitrary key to distinguish 29 | // messages sent through different streams (connections). 30 | type Sub interface { 31 | Listen(ctx context.Context) error 32 | Receive(ctx context.Context, p []byte) (payload []byte, stream StreamDescriptor, err error) 33 | } 34 | 35 | // SubOpts to tweak 36 | type SubOpts struct { 37 | KeepaliveTimeout time.Duration // how long to keep a stream alive if there are no new messages 38 | Printf func(format string, v ...interface{}) // Printf optionally specifies a function used for logging 39 | } 40 | 41 | type sub struct { 42 | SubOpts 43 | addr net.Addr 44 | queue *ringBuf // in-memory buffer to accumulate incoming messages 45 | mu sync.Mutex 46 | hasData *sync.Cond 47 | e encoder 48 | pool pool 49 | scratchpad [4]byte // used to prepend messages with the uint32 streamID 50 | metrics *Metrics 51 | numStreams prometheus.Gauge 52 | streamDurationSec prometheus.Observer 53 | lostBytes prometheus.Counter 54 | lostMsgs prometheus.Counter 55 | } 56 | 57 | // NewSub constructs a Sub 58 | func NewSub(url string, opts SubOpts, metr *Metrics) Sub { 59 | addr := MustParseURL(url) 60 | l := prometheus.Labels{lAddr: addr.String()} 61 | 62 | s := &sub{ 63 | SubOpts: opts, 64 | addr: addr, 65 | queue: &ringBuf{}, 66 | pool: pool{New: func() interface{} { return newIStream() }}, 67 | // metrics 68 | metrics: metr, 69 | numStreams: metr.numStreams.With(l), 70 | streamDurationSec: metr.streamDurationSec.With(l), 71 | lostBytes: metr.lostBytes.With(l), 72 | lostMsgs: metr.lostMsgs.With(l), 73 | } 74 | s.hasData = sync.NewCond(&s.mu) 75 | 76 | return s 77 | } 78 | 79 | func (s *sub) enqueue(streamID []byte, payload []byte) { 80 | s.mu.Lock() 81 | defer s.mu.Unlock() 82 | 83 | if n, err := s.queue.Write(streamID); err != nil || n != len(streamID) { 84 | panic("nanoq: failed to write streamID into the internal queue") 85 | } 86 | if err := s.e.EncodeTo(s.queue, payload); err != nil { 87 | panic("nanoq: failed to write payload into the internal queue") 88 | } 89 | s.hasData.Signal() 90 | } 91 | 92 | func (s *sub) dequeue(p []byte) ([]byte, StreamDescriptor, error) { 93 | s.mu.Lock() 94 | defer s.mu.Unlock() 95 | 96 | if s.queue.Len() == 0 { 97 | return nil, StreamDescriptor(0), nil 98 | } 99 | 100 | // decode stream descriptor 101 | if _, err := io.ReadFull(s.queue, s.scratchpad[:]); err != nil { 102 | panic("nanoq: failed to read streamID from the internal queue") 103 | } 104 | stream := StreamDescriptor(binary.LittleEndian.Uint32(s.scratchpad[:])) 105 | // decode payload size 106 | size, err := decodeSize(s.queue) 107 | if err != nil { 108 | panic("nanoq: failed to decode payload size from the internal queue: " + err.Error()) 109 | } 110 | // decode payload and store data to byte slice p 111 | if cap(p) < size { 112 | if err := s.queue.Discard(size); err != nil { 113 | panic("nanoq: failed to discard payload from the internal queue") 114 | } 115 | s.lostMsgs.Inc() 116 | s.lostBytes.Add(float64(size)) 117 | return nil, StreamDescriptor(0), io.ErrShortBuffer 118 | } 119 | p = p[:cap(p)] // reslice for max capacity 120 | 121 | n, err := io.ReadFull(s.queue, p[:size]) 122 | if err != nil { 123 | panic("nanoq: failed to read payload from the internal queue: " + err.Error()) 124 | } 125 | if n != size { 126 | panic("nanoq: invalid read from the internal queue") 127 | } 128 | return p[:n], stream, nil 129 | } 130 | 131 | func (s *sub) Receive(ctx context.Context, p []byte) (payload []byte, stream StreamDescriptor, err error) { 132 | // wait in a loop for data 133 | for payload == nil { 134 | if payload, stream, err = s.dequeue(p); err != nil || payload != nil { 135 | return // either we get data or read error 136 | } 137 | 138 | // check for cancelation 139 | select { 140 | case <-ctx.Done(): 141 | return nil, StreamDescriptor(0), ctx.Err() 142 | default: 143 | } 144 | 145 | // wait on Cond variable 146 | s.mu.Lock() 147 | s.hasData.Wait() 148 | s.mu.Unlock() 149 | } 150 | return 151 | } 152 | 153 | func (s *sub) Listen(ctx context.Context) error { 154 | var ( 155 | wg sync.WaitGroup 156 | listenConf net.ListenConfig 157 | ln net.Listener 158 | err error 159 | ) 160 | 161 | defer wg.Wait() 162 | 163 | if ln, err = listenConf.Listen(ctx, s.addr.Network(), s.addr.String()); err != nil { 164 | return fmt.Errorf("nanoq: failed to listen: %w", err) 165 | } 166 | 167 | out(s.Printf, "listen %s", ln.Addr().String()) 168 | 169 | // monitor shutdown request to force close the listener 170 | var shutdown int32 171 | const graceful int32 = 1 172 | go func() { 173 | <-ctx.Done() 174 | atomic.StoreInt32(&shutdown, graceful) 175 | ln.Close() 176 | // Notify receivers 177 | s.mu.Lock() 178 | s.hasData.Broadcast() 179 | s.mu.Unlock() 180 | }() 181 | 182 | var streamID uint32 183 | // accept connections 184 | for { 185 | conn, err := ln.Accept() 186 | if err != nil { 187 | if atomic.LoadInt32(&shutdown) == graceful { 188 | return ctx.Err() 189 | } 190 | return fmt.Errorf("nanoq: failed to accept: %w", err) 191 | } 192 | s.numStreams.Inc() 193 | out(s.Printf, "new connection from %s", conn.RemoteAddr()) 194 | streamID++ 195 | wg.Add(1) 196 | // connection 197 | go func(streamID uint32) { 198 | start := time.Now() 199 | // Reader buffer for conn 200 | is := s.pool.Get().(*istream) 201 | // receive loop 202 | if err := is.Work(ctx, s, streamID, conn); err != nil { 203 | out(s.Printf, "connection from %s: %s", conn.RemoteAddr(), err) 204 | } 205 | // tear down 206 | closeConn(s.Printf, conn, "from") 207 | s.pool.Put(is) 208 | s.numStreams.Dec() 209 | s.streamDurationSec.Observe(time.Since(start).Seconds()) 210 | wg.Done() 211 | }(streamID) 212 | } 213 | } 214 | --------------------------------------------------------------------------------