From d4a8ec0754aba98416c4f8dd022b4364527d94cc Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 20 Mar 2024 08:29:08 +0100 Subject: [PATCH] feat(client): v3: ilp over http (#26) Co-authored-by: Andrei Pechkurov <37772591+puzpuzpuz@users.noreply.github.com> --- .gitmodules | 4 +- README.md | 38 +- buffer.go | 597 +++++++++ buffer_test.go | 449 +++++++ conf_errors.go | 44 + conf_parse.go | 243 ++++ conf_test.go | 465 +++++++ examples.manifest.yaml | 19 +- examples/from-conf/main.go | 61 + examples/http/auth-and-tls/main.go | 69 + examples/http/auth/main.go | 69 + examples/http/basic/main.go | 62 + examples/{ => tcp}/auth-and-tls/main.go | 4 +- examples/{ => tcp}/auth/main.go | 4 +- examples/{ => tcp}/basic/main.go | 4 +- export_test.go | 78 ++ go.mod | 44 +- go.sum | 1040 +-------------- http_errors.go | 79 ++ http_integration_test.go | 124 ++ http_sender.go | 434 +++++++ http_sender_test.go | 623 +++++++++ integration_test.go | 456 +++++++ sender_interop_test.go => interop_test.go | 74 +- sender.go | 1152 +++++++---------- sender_integration_test.go | 743 ----------- sender_test.go | 787 ----------- tcp_integration_test.go | 382 ++++++ tcp_sender.go | 266 ++++ tcp_sender_test.go | 266 ++++ test/haproxy.cfg | 10 + test/{interop => interop/questdb-client-test} | 0 utils_test.go | 253 ++++ 33 files changed, 5667 insertions(+), 3276 deletions(-) create mode 100644 buffer.go create mode 100644 buffer_test.go create mode 100644 conf_errors.go create mode 100644 conf_parse.go create mode 100644 conf_test.go create mode 100644 examples/from-conf/main.go create mode 100644 examples/http/auth-and-tls/main.go create mode 100644 examples/http/auth/main.go create mode 100644 examples/http/basic/main.go rename examples/{ => tcp}/auth-and-tls/main.go (94%) rename examples/{ => tcp}/auth/main.go (94%) rename examples/{ => tcp}/basic/main.go (93%) create mode 100644 export_test.go create mode 100644 http_errors.go create mode 100644 http_integration_test.go create mode 100644 http_sender.go create mode 100644 http_sender_test.go create mode 100644 integration_test.go rename sender_interop_test.go => interop_test.go (63%) delete mode 100644 sender_integration_test.go delete mode 100644 sender_test.go create mode 100644 tcp_integration_test.go create mode 100644 tcp_sender.go create mode 100644 tcp_sender_test.go rename test/{interop => interop/questdb-client-test} (100%) create mode 100644 utils_test.go diff --git a/.gitmodules b/.gitmodules index 0719396..c0bf819 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "test/interop"] - path = test/interop +[submodule "test/interop/questdb-client-test"] + path = test/interop/questdb-client-test url = https://github.com/questdb/questdb-client-test.git diff --git a/README.md b/README.md index ed8300a..f7deb4d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ -[![GoDoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/questdb/go-questdb-client/v2) +[![GoDoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/questdb/go-questdb-client/v3) # go-questdb-client -Golang client for QuestDB's Influx Line Protocol over TCP. +Golang client for QuestDB's [Influx Line Protocol](https://questdb.io/docs/reference/api/ilp/overview/) +(ILP) over HTTP and TCP. This library makes it easy to insert data into +[QuestDB](https://questdb.io). Features: * Context-aware API. * Optimized for batch writes. -* Supports TLS encryption and [ILP authentication](https://questdb.io/docs/reference/api/ilp/authenticate). -* Tested against QuestDB 7.3.2 and newer versions. +* Supports TLS encryption and ILP authentication. +* Automatic write retries and connection reuse for ILP over HTTP. +* Tested against QuestDB 7.3.11 and newer versions. -Documentation is available [here](https://pkg.go.dev/github.com/questdb/go-questdb-client/v2). +New in v3: +* Supports ILP over HTTP using the same client semantics -## Usage +Documentation is available [here](https://pkg.go.dev/github.com/questdb/go-questdb-client/v3). + +## Quickstart ```go package main @@ -23,13 +29,13 @@ import ( "log" "time" - qdb "github.com/questdb/go-questdb-client/v2" + qdb "github.com/questdb/go-questdb-client/v3" ) func main() { ctx := context.TODO() - // Connect to QuestDB running on 127.0.0.1:9009 - sender, err := qdb.NewLineSender(ctx) + // Connect to QuestDB running locally. + sender, err := qdb.LineSenderFromConf(ctx, "http::addr=localhost:9000;") if err != nil { log.Fatal(err) } @@ -59,3 +65,17 @@ func main() { } } ``` + +To connect via TCP, set the configuration string to: +```go + // ... + sender, err := qdb.LineSenderFromConf(ctx, "tcp::addr=localhost:9009;") + // ... +``` + +## Community + +If you need help, have additional questions or want to provide feedback, you +may find us on [Slack](https://slack.questdb.io). +You can also [sign up to our mailing list](https://questdb.io/community/) +to get notified of new releases. diff --git a/buffer.go b/buffer.go new file mode 100644 index 0000000..41cce8d --- /dev/null +++ b/buffer.go @@ -0,0 +1,597 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "math/big" + "strconv" + "time" +) + +// errInvalidMsg indicates a failed attempt to construct an ILP +// message, e.g. duplicate calls to Table method or illegal +// chars found in table or column name. +var errInvalidMsg = errors.New("invalid message") + +// buffer is a wrapper on top of bytes.Buffer. It extends the +// original struct with methods for writing int64 and float64 +// numbers without unnecessary allocations. +type buffer struct { + bytes.Buffer + + initBufSize int + maxBufSize int + fileNameLimit int + + lastMsgPos int + lastErr error + hasTable bool + hasTags bool + hasFields bool + msgCount int +} + +func newBuffer(initBufSize int, maxBufSize int, fileNameLimit int) buffer { + var b buffer + b.initBufSize = initBufSize + b.maxBufSize = maxBufSize + b.fileNameLimit = fileNameLimit + b.ResetSize() + return b +} + +func (b *buffer) ResetSize() { + b.Buffer = *bytes.NewBuffer(make([]byte, 0, b.initBufSize)) +} + +func (b *buffer) HasTable() bool { + return b.hasTable +} + +func (b *buffer) HasTags() bool { + return b.hasTags +} + +func (b *buffer) HasFields() bool { + return b.hasFields +} + +func (b *buffer) LastErr() error { + return b.lastErr +} + +func (b *buffer) ClearLastErr() { + b.lastErr = nil +} + +func (b *buffer) writeInt(i int64) { + // We need up to 20 bytes to fit an int64, including a sign. + var a [20]byte + s := strconv.AppendInt(a[0:0], i, 10) + b.Write(s) +} + +func (b *buffer) writeFloat(f float64) { + if math.IsNaN(f) { + b.WriteString("NaN") + return + } else if math.IsInf(f, -1) { + b.WriteString("-Infinity") + return + } else if math.IsInf(f, 1) { + b.WriteString("Infinity") + return + } + // We need up to 24 bytes to fit a float64, including a sign. + var a [24]byte + s := strconv.AppendFloat(a[0:0], f, 'G', -1, 64) + b.Write(s) +} + +func (b *buffer) writeBigInt(i *big.Int) { + // We need up to 64 bytes to fit an unsigned 256-bit number. + var a [64]byte + s := i.Append(a[0:0], 16) + b.Write(s) +} + +// WriteTo wraps the built-in bytes.Buffer.WriteTo method +// and writes the contents of the buffer to the provided +// io.Writer +func (b *buffer) WriteTo(w io.Writer) (int64, error) { + n, err := b.Buffer.WriteTo(w) + if err != nil { + b.lastMsgPos -= int(n) + return n, err + } + b.lastMsgPos = 0 + b.msgCount = 0 + return n, nil +} + +func (b *buffer) writeTableName(str string) error { + if str == "" { + return fmt.Errorf("table name cannot be empty: %w", errInvalidMsg) + } + // We use string length in bytes as an approximation. That's to + // avoid calculating the number of runes. + if len(str) > b.fileNameLimit { + return fmt.Errorf("table name length exceeds the limit: %w", errInvalidMsg) + } + // Since we're interested in ASCII chars, it's fine to iterate + // through bytes instead of runes. + for i := 0; i < len(str); i++ { + ch := str[i] + switch ch { + case ' ': + b.WriteByte('\\') + case '=': + b.WriteByte('\\') + case '.': + if i == 0 || i == len(str)-1 { + return fmt.Errorf("table name contains '.' char at the start or end: %s: %w", str, errInvalidMsg) + } + default: + if illegalTableNameChar(ch) { + return fmt.Errorf("table name contains an illegal char: "+ + "'\\n', '\\r', '?', ',', ''', '\"', '\\', '/', ':', ')', '(', '+', '*' '%%', '~', or a non-printable char: %s: %w", + str, errInvalidMsg) + } + } + b.WriteByte(ch) + } + return nil +} + +func illegalTableNameChar(ch byte) bool { + switch ch { + case '\n': + return true + case '\r': + return true + case '?': + return true + case ',': + return true + case '\'': + return true + case '"': + return true + case '\\': + return true + case '/': + return true + case ':': + return true + case ')': + return true + case '(': + return true + case '+': + return true + case '*': + return true + case '%': + return true + case '~': + return true + case '\u0000': + return true + case '\u0001': + return true + case '\u0002': + return true + case '\u0003': + return true + case '\u0004': + return true + case '\u0005': + return true + case '\u0006': + return true + case '\u0007': + return true + case '\u0008': + return true + case '\u0009': + return true + case '\u000b': + return true + case '\u000c': + return true + case '\u000e': + return true + case '\u000f': + return true + case '\u007f': + return true + } + return false +} + +func (b *buffer) writeColumnName(str string) error { + if str == "" { + return fmt.Errorf("column name cannot be empty: %w", errInvalidMsg) + } + // We use string length in bytes as an approximation. That's to + // avoid calculating the number of runes. + if len(str) > b.fileNameLimit { + return fmt.Errorf("column name length exceeds the limit: %w", errInvalidMsg) + } + // Since we're interested in ASCII chars, it's fine to iterate + // through bytes instead of runes. + for i := 0; i < len(str); i++ { + ch := str[i] + switch ch { + case ' ': + b.WriteByte('\\') + case '=': + b.WriteByte('\\') + default: + if illegalColumnNameChar(ch) { + return fmt.Errorf("column name contains an illegal char: "+ + "'\\n', '\\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+', '-', '*' '%%', '~', or a non-printable char: %s: %w", + str, errInvalidMsg) + } + } + b.WriteByte(ch) + } + return nil +} + +func illegalColumnNameChar(ch byte) bool { + switch ch { + case '\n': + return true + case '\r': + return true + case '?': + return true + case '.': + return true + case ',': + return true + case '\'': + return true + case '"': + return true + case '\\': + return true + case '/': + return true + case ':': + return true + case ')': + return true + case '(': + return true + case '+': + return true + case '-': + return true + case '*': + return true + case '%': + return true + case '~': + return true + case '\u0000': + return true + case '\u0001': + return true + case '\u0002': + return true + case '\u0003': + return true + case '\u0004': + return true + case '\u0005': + return true + case '\u0006': + return true + case '\u0007': + return true + case '\u0008': + return true + case '\u0009': + return true + case '\u000b': + return true + case '\u000c': + return true + case '\u000e': + return true + case '\u000f': + return true + case '\u007f': + return true + } + return false +} + +func (b *buffer) writeStrValue(str string, quoted bool) error { + // Since we're interested in ASCII chars, it's fine to iterate + // through bytes instead of runes. + for i := 0; i < len(str); i++ { + ch := str[i] + switch ch { + case ' ': + if !quoted { + b.WriteByte('\\') + } + case ',': + if !quoted { + b.WriteByte('\\') + } + case '=': + if !quoted { + b.WriteByte('\\') + } + case '"': + if quoted { + b.WriteByte('\\') + } + case '\n': + b.WriteByte('\\') + case '\r': + b.WriteByte('\\') + case '\\': + b.WriteByte('\\') + } + b.WriteByte(ch) + } + return nil +} + +func (b *buffer) prepareForField() bool { + if b.lastErr != nil { + return false + } + if !b.hasTable { + b.lastErr = fmt.Errorf("table name was not provided: %w", errInvalidMsg) + return false + } + if !b.hasFields { + b.WriteByte(' ') + } else { + b.WriteByte(',') + } + return true +} + +func (b *buffer) DiscardPendingMsg() { + b.Truncate(b.lastMsgPos) + b.resetMsgFlags() +} + +func (b *buffer) resetMsgFlags() { + b.hasTable = false + b.hasTags = false + b.hasFields = false +} + +func (b *buffer) Messages() string { + return b.String() +} + +func (b *buffer) Table(name string) *buffer { + if b.lastErr != nil { + return b + } + if b.hasTable { + b.lastErr = fmt.Errorf("table name already provided: %w", errInvalidMsg) + return b + } + b.lastErr = b.writeTableName(name) + if b.lastErr != nil { + return b + } + b.hasTable = true + return b +} + +func (b *buffer) Symbol(name, val string) *buffer { + if b.lastErr != nil { + return b + } + if !b.hasTable { + b.lastErr = fmt.Errorf("table name was not provided: %w", errInvalidMsg) + return b + } + if b.hasFields { + b.lastErr = fmt.Errorf("symbols have to be written before any other column: %w", errInvalidMsg) + return b + } + b.WriteByte(',') + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + b.lastErr = b.writeStrValue(val, false) + if b.lastErr != nil { + return b + } + b.hasTags = true + return b +} + +func (b *buffer) Int64Column(name string, val int64) *buffer { + if !b.prepareForField() { + return b + } + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + b.writeInt(val) + b.WriteByte('i') + b.hasFields = true + return b +} + +func (b *buffer) Long256Column(name string, val *big.Int) *buffer { + if val.Sign() < 0 { + if b.lastErr != nil { + return b + } + b.lastErr = fmt.Errorf("long256 cannot be negative: %s", val.String()) + return b + } + if val.BitLen() > 256 { + if b.lastErr != nil { + return b + } + b.lastErr = fmt.Errorf("long256 cannot be larger than 256-bit: %v", val.BitLen()) + return b + } + if !b.prepareForField() { + return b + } + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + b.WriteByte('0') + b.WriteByte('x') + b.writeBigInt(val) + b.WriteByte('i') + if b.lastErr != nil { + return b + } + b.hasFields = true + return b +} + +func (b *buffer) TimestampColumn(name string, ts time.Time) *buffer { + if !b.prepareForField() { + return b + } + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + b.writeInt(ts.UnixMicro()) + b.WriteByte('t') + b.hasFields = true + return b +} + +func (b *buffer) Float64Column(name string, val float64) *buffer { + if !b.prepareForField() { + return b + } + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + b.writeFloat(val) + b.hasFields = true + return b +} + +func (b *buffer) StringColumn(name, val string) *buffer { + if !b.prepareForField() { + return b + } + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + b.WriteByte('"') + b.lastErr = b.writeStrValue(val, true) + if b.lastErr != nil { + return b + } + b.WriteByte('"') + b.hasFields = true + return b +} + +func (b *buffer) BoolColumn(name string, val bool) *buffer { + if !b.prepareForField() { + return b + } + b.lastErr = b.writeColumnName(name) + if b.lastErr != nil { + return b + } + b.WriteByte('=') + if val { + b.WriteByte('t') + } else { + b.WriteByte('f') + } + b.hasFields = true + return b +} + +func (b *buffer) At(ts time.Time, sendTs bool) error { + err := b.lastErr + b.lastErr = nil + if err != nil { + b.DiscardPendingMsg() + return err + } + + // Post-factum check for the max buffer size limit. + // Since we embed bytes.Buffer, it's impossible to hook into its + // grow() method properly to have the check before we write + // a value to the buffer. + if b.maxBufSize > 0 && b.Cap() > b.maxBufSize { + b.DiscardPendingMsg() + return fmt.Errorf("buffer size exceeded maximum limit: size=%d, limit=%d", b.Cap(), b.maxBufSize) + } + + if !b.hasTable { + b.DiscardPendingMsg() + return fmt.Errorf("table name was not provided: %w", errInvalidMsg) + } + if !b.hasTags && !b.hasFields { + b.DiscardPendingMsg() + return fmt.Errorf("no symbols or columns were provided: %w", errInvalidMsg) + } + + if sendTs { + b.WriteByte(' ') + b.writeInt(ts.UnixNano()) + } + b.WriteByte('\n') + + b.lastMsgPos = b.Len() + b.msgCount++ + b.resetMsgFlags() + return nil +} diff --git a/buffer_test.go b/buffer_test.go new file mode 100644 index 0000000..515d9a9 --- /dev/null +++ b/buffer_test.go @@ -0,0 +1,449 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "math" + "math/big" + "strconv" + "strings" + "testing" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" + "github.com/stretchr/testify/assert" +) + +type bufWriterFn func(b *qdb.Buffer) error + +func newTestBuffer() qdb.Buffer { + return qdb.NewBuffer(128*1024, 1024*1024, 127) +} + +func TestValidWrites(t *testing.T) { + testCases := []struct { + name string + writerFn bufWriterFn + expectedLines []string + }{ + { + "multiple rows", + func(b *qdb.Buffer) error { + err := b.Table(testTable).StringColumn("str_col", "foo").Int64Column("long_col", 42).At(time.Time{}, false) + if err != nil { + return err + } + err = b.Table(testTable).StringColumn("str_col", "bar").Int64Column("long_col", -42).At(time.UnixMicro(42), true) + if err != nil { + return err + } + return nil + }, + []string{ + "my_test_table str_col=\"foo\",long_col=42i", + "my_test_table str_col=\"bar\",long_col=-42i 42000", + }, + }, + { + "UTF-8 strings", + func(s *qdb.Buffer) error { + return s.Table("таблица").StringColumn("колонка", "значение").At(time.Time{}, false) + }, + []string{ + "таблица колонка=\"значение\"", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + err := tc.writerFn(&buf) + assert.NoError(t, err) + + // Check the buffer + assert.Equal(t, strings.Join(tc.expectedLines, "\n")+"\n", buf.Messages()) + + }) + } +} + +func TestTimestampSerialization(t *testing.T) { + testCases := []struct { + name string + val time.Time + }{ + {"max value", time.UnixMicro(math.MaxInt64)}, + {"zero", time.UnixMicro(0)}, + {"small positive value", time.UnixMicro(10)}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table(testTable).TimestampColumn("a_col", tc.val).At(time.Time{}, false) + assert.NoError(t, err) + + // Check the buffer + expectedLines := []string{"my_test_table a_col=" + strconv.FormatInt(tc.val.UnixMicro(), 10) + "t"} + assert.Equal(t, strings.Join(expectedLines, "\n")+"\n", buf.Messages()) + }) + } +} + +func TestInt64Serialization(t *testing.T) { + testCases := []struct { + name string + val int64 + }{ + {"min value", math.MinInt64}, + {"max value", math.MaxInt64}, + {"zero", 0}, + {"small negative value", -10}, + {"small positive value", 10}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table(testTable).Int64Column("a_col", tc.val).At(time.Time{}, false) + assert.NoError(t, err) + + // Check the buffer + expectedLines := []string{"my_test_table a_col=" + strconv.FormatInt(tc.val, 10) + "i"} + assert.Equal(t, strings.Join(expectedLines, "\n")+"\n", buf.Messages()) + }) + } +} + +func TestLong256Column(t *testing.T) { + testCases := []struct { + name string + val string + expected string + }{ + {"zero", "0", "0x0"}, + {"one", "1", "0x1"}, + {"32-bit max", strconv.FormatInt(math.MaxInt32, 16), "0x7fffffff"}, + {"64-bit random", strconv.FormatInt(7423093023234231, 16), "0x1a5f4386c8d8b7"}, + {"256-bit max", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + newVal, _ := big.NewInt(0).SetString(tc.val, 16) + err := buf.Table(testTable).Long256Column("a_col", newVal).At(time.Time{}, false) + assert.NoError(t, err) + + // Check the buffer + expectedLines := []string{"my_test_table a_col=" + tc.expected + "i"} + assert.Equal(t, strings.Join(expectedLines, "\n")+"\n", buf.Messages()) + }) + } +} + +func TestFloat64Serialization(t *testing.T) { + testCases := []struct { + name string + val float64 + expected string + }{ + {"NaN", math.NaN(), "NaN"}, + {"positive infinity", math.Inf(1), "Infinity"}, + {"negative infinity", math.Inf(-1), "-Infinity"}, + {"negative infinity", math.Inf(-1), "-Infinity"}, + {"positive number", 42.3, "42.3"}, + {"negative number", -42.3, "-42.3"}, + {"smallest value", math.SmallestNonzeroFloat64, "5E-324"}, + {"max value", math.MaxFloat64, "1.7976931348623157E+308"}, + {"negative with exponent", -4.2e-99, "-4.2E-99"}, + {"small with exponent", 4.2e-99, "4.2E-99"}, + {"large with exponent", 4.2e99, "4.2E+99"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table(testTable).Float64Column("a_col", tc.val).At(time.Time{}, false) + assert.NoError(t, err) + + // Check the buffer + expectedLines := []string{"my_test_table a_col=" + tc.expected} + assert.Equal(t, strings.Join(expectedLines, "\n")+"\n", buf.Messages()) + }) + } +} + +func TestErrorOnTooLargeBuffer(t *testing.T) { + const initBufSize = 1 + const maxBufSize = 4 + + testCases := []struct { + name string + writerFn bufWriterFn + }{ + { + "table name and ts", + func(s *qdb.Buffer) error { + return s.Table("foobar").At(time.Time{}, false) + }, + }, + { + "string column", + func(s *qdb.Buffer) error { + return s.Table("a").StringColumn("str_col", "foo").At(time.Time{}, false) + }, + }, + { + "long column", + func(s *qdb.Buffer) error { + return s.Table("a").Int64Column("str_col", 1000000).At(time.Time{}, false) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := qdb.NewBuffer(initBufSize, maxBufSize, 127) + + err := tc.writerFn(&buf) + assert.ErrorContains(t, err, "buffer size exceeded maximum limit") + }) + } +} + +func TestErrorOnLengthyNames(t *testing.T) { + const nameLimit = 42 + + var ( + lengthyStr = strings.Repeat("a", nameLimit+1) + ) + + testCases := []struct { + name string + writerFn bufWriterFn + expectedErrMsg string + }{ + { + "lengthy table name", + func(s *qdb.Buffer) error { + return s.Table(lengthyStr).StringColumn("str_col", "foo").At(time.Time{}, false) + }, + "table name length exceeds the limit", + }, + { + "lengthy column name", + func(s *qdb.Buffer) error { + return s.Table(testTable).StringColumn(lengthyStr, "foo").At(time.Time{}, false) + }, + "column name length exceeds the limit", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := qdb.NewBuffer(128*1024, 1024*1024, nameLimit) + + err := tc.writerFn(&buf) + assert.ErrorContains(t, err, tc.expectedErrMsg) + assert.Empty(t, buf.Messages()) + }) + } +} + +func TestErrorOnMissingTableCall(t *testing.T) { + testCases := []struct { + name string + writerFn bufWriterFn + }{ + { + "At", + func(s *qdb.Buffer) error { + return s.Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + { + "symbol", + func(s *qdb.Buffer) error { + return s.Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + { + "string column", + func(s *qdb.Buffer) error { + return s.StringColumn("str", "abc").At(time.Time{}, false) + }, + }, + { + "boolean column", + func(s *qdb.Buffer) error { + return s.BoolColumn("bool", true).At(time.Time{}, false) + }, + }, + { + "long column", + func(s *qdb.Buffer) error { + return s.Int64Column("int", 42).At(time.Time{}, false) + }, + }, + { + "double column", + func(s *qdb.Buffer) error { + return s.Float64Column("float", 4.2).At(time.Time{}, false) + }, + }, + { + "timestamp column", + func(s *qdb.Buffer) error { + return s.TimestampColumn("timestamp", time.UnixMicro(42)).At(time.Time{}, false) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + err := tc.writerFn(&buf) + + assert.ErrorContains(t, err, "table name was not provided") + assert.Empty(t, buf.Messages()) + }) + } +} + +func TestErrorOnMultipleTableCalls(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table(testTable).Table(testTable).At(time.Time{}, false) + + assert.ErrorContains(t, err, "table name already provided") + assert.Empty(t, buf.Messages()) +} + +func TestErrorOnNegativeLong256(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table(testTable).Long256Column("long256_col", big.NewInt(-42)).At(time.Time{}, false) + + assert.ErrorContains(t, err, "long256 cannot be negative: -42") + assert.Empty(t, buf.Messages()) +} + +func TestErrorOnLargerLong256(t *testing.T) { + buf := newTestBuffer() + + bigVal, _ := big.NewInt(0).SetString("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + err := buf.Table(testTable).Long256Column("long256_col", bigVal).At(time.Time{}, false) + + assert.ErrorContains(t, err, "long256 cannot be larger than 256-bit: 260") + assert.Empty(t, buf.Messages()) +} + +func TestErrorOnSymbolCallAfterColumn(t *testing.T) { + testCases := []struct { + name string + writerFn bufWriterFn + }{ + { + "string column", + func(s *qdb.Buffer) error { + return s.Table("awesome_table").StringColumn("str", "abc").Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + { + "boolean column", + func(s *qdb.Buffer) error { + return s.Table("awesome_table").BoolColumn("bool", true).Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + { + "integer column", + func(s *qdb.Buffer) error { + return s.Table("awesome_table").Int64Column("int", 42).Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + { + "float column", + func(s *qdb.Buffer) error { + return s.Table("awesome_table").Float64Column("float", 4.2).Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + { + "timestamp column", + func(s *qdb.Buffer) error { + return s.Table("awesome_table").TimestampColumn("timestamp", time.UnixMicro(42)).Symbol("sym", "abc").At(time.Time{}, false) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := newTestBuffer() + + err := tc.writerFn(&buf) + + assert.ErrorContains(t, err, "symbols have to be written before any other column") + assert.Empty(t, buf.Messages()) + }) + } +} + +func TestInvalidMessageGetsDiscarded(t *testing.T) { + buf := newTestBuffer() + + // Write a valid message. + err := buf.Table(testTable).StringColumn("foo", "bar").At(time.Time{}, false) + assert.NoError(t, err) + // Then write perform an incorrect chain of calls. + err = buf.Table(testTable).StringColumn("foo", "bar").Symbol("sym", "42").At(time.Time{}, false) + assert.Error(t, err) + + // The second message should be discarded. + expectedLines := []string{testTable + " foo=\"bar\""} + assert.Equal(t, strings.Join(expectedLines, "\n")+"\n", buf.Messages()) +} + +func TestInvalidTableName(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table("invalid\ntable").StringColumn("foo", "bar").At(time.Time{}, false) + assert.ErrorContains(t, err, "table name contains an illegal char") + assert.Empty(t, buf.Messages()) +} + +func TestInvalidColumnName(t *testing.T) { + buf := newTestBuffer() + + err := buf.Table(testTable).StringColumn("invalid\ncolumn", "bar").At(time.Time{}, false) + assert.ErrorContains(t, err, "column name contains an illegal char") + assert.Empty(t, buf.Messages()) +} diff --git a/conf_errors.go b/conf_errors.go new file mode 100644 index 0000000..d7de688 --- /dev/null +++ b/conf_errors.go @@ -0,0 +1,44 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +import "fmt" + +// InvalidConfigStrError is error indicating invalid config string. +type InvalidConfigStrError struct { + msg string +} + +// Error returns full error message string. +func (e *InvalidConfigStrError) Error() string { + return fmt.Sprintf("Error parsing config string: %q", e.msg) +} + +// NewInvalidConfigStrError creates new InvalidConfigStrError. +func NewInvalidConfigStrError(msg string, args ...interface{}) *InvalidConfigStrError { + return &InvalidConfigStrError{ + msg: fmt.Sprintf(msg, args...), + } +} diff --git a/conf_parse.go b/conf_parse.go new file mode 100644 index 0000000..7fa823e --- /dev/null +++ b/conf_parse.go @@ -0,0 +1,243 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +type configData struct { + Schema string + KeyValuePairs map[string]string +} + +func confFromStr(conf string) (*lineSenderConfig, error) { + senderConf := &lineSenderConfig{} + + data, err := parseConfigStr(conf) + if err != nil { + return nil, err + } + + switch data.Schema { + case "http": + senderConf.senderType = httpSenderType + case "https": + senderConf.senderType = httpSenderType + senderConf.tlsMode = tlsEnabled + case "tcp": + senderConf.senderType = tcpSenderType + case "tcps": + senderConf.senderType = tcpSenderType + senderConf.tlsMode = tlsEnabled + default: + return nil, fmt.Errorf("invalid schema: %s", data.Schema) + } + + for k, v := range data.KeyValuePairs { + switch strings.ToLower(k) { + case "addr": + senderConf.address = v + case "username": + switch senderConf.senderType { + case httpSenderType: + senderConf.httpUser = v + case tcpSenderType: + senderConf.tcpKeyId = v + default: + panic("add a case for " + k) + } + case "password": + if senderConf.senderType != httpSenderType { + return nil, NewInvalidConfigStrError("%s is only supported for HTTP sender", k) + } + senderConf.httpPass = v + case "token": + switch senderConf.senderType { + case httpSenderType: + senderConf.httpToken = v + case tcpSenderType: + senderConf.tcpKey = v + default: + panic("add a case for " + k) + } + case "auto_flush": + if v == "off" { + senderConf.autoFlushRows = 0 + senderConf.autoFlushInterval = 0 + } else if v != "on" { + return nil, NewInvalidConfigStrError("invalid %s value, %q is not 'on' or 'off'", k, v) + } + case "auto_flush_rows": + parsedVal, err := strconv.Atoi(v) + if err != nil { + return nil, NewInvalidConfigStrError("invalid %s value, %q is not a valid int", k, v) + } + senderConf.autoFlushRows = parsedVal + case "auto_flush_interval": + parsedVal, err := strconv.Atoi(v) + if err != nil { + return nil, NewInvalidConfigStrError("invalid %s value, %q is not a valid int", k, v) + } + senderConf.autoFlushInterval = time.Duration(parsedVal) + case "min_throughput", "init_buf_size", "max_buf_size": + parsedVal, err := strconv.Atoi(v) + if err != nil { + return nil, NewInvalidConfigStrError("invalid %s value, %q is not a valid int", k, v) + } + + switch k { + case "min_throughput": + senderConf.minThroughput = parsedVal + case "init_buf_size": + senderConf.initBufSize = parsedVal + case "max_buf_size": + senderConf.maxBufSize = parsedVal + default: + panic("add a case for " + k) + } + case "request_timeout", "retry_timeout": + timeout, err := strconv.Atoi(v) + if err != nil { + return nil, NewInvalidConfigStrError("invalid %s value, %q is not a valid int", k, v) + } + timeoutDur := time.Duration(timeout * int(time.Millisecond)) + + switch k { + case "request_timeout": + senderConf.requestTimeout = timeoutDur + case "retry_timeout": + senderConf.retryTimeout = timeoutDur + default: + panic("add a case for " + k) + } + case "tls_verify": + switch v { + case "on": + senderConf.tlsMode = tlsEnabled + case "unsafe_off": + senderConf.tlsMode = tlsInsecureSkipVerify + default: + return nil, NewInvalidConfigStrError("invalid tls_verify value, %q is not 'on' or 'unsafe_off", v) + } + case "tls_roots": + return nil, NewInvalidConfigStrError("tls_roots is not available in the go client") + case "tls_roots_password": + return nil, NewInvalidConfigStrError("tls_roots_password is not available in the go client") + default: + return nil, NewInvalidConfigStrError("unsupported option %q", k) + } + } + + return senderConf, nil +} + +func parseConfigStr(conf string) (configData, error) { + var ( + key = &strings.Builder{} + value = &strings.Builder{} + isKey = true + result = configData{ + KeyValuePairs: map[string]string{}, + } + + nextRune rune + isEscaping bool + hasTrailingSemicolon bool + ) + + schemaStr, conf, found := strings.Cut(conf, "::") + if !found { + return result, NewInvalidConfigStrError("no schema separator found '::'") + } + + result.Schema = schemaStr + + if len(conf) == 0 { + return result, NewInvalidConfigStrError("'addr' key not found") + } + + if strings.HasSuffix(conf, ";") { + hasTrailingSemicolon = true + } else { + conf = conf + ";" // add trailing semicolon if it doesn't exist + } + + keyValueStr := []rune(conf) + for idx, rune := range keyValueStr { + if idx < len(conf)-1 { + nextRune = keyValueStr[idx+1] + } else { + nextRune = 0 + } + switch rune { + case ';': + if isKey { + if nextRune == 0 && !hasTrailingSemicolon { + return result, NewInvalidConfigStrError("unexpected end of string") + } + return result, NewInvalidConfigStrError("invalid key character ';'") + } + + if !isEscaping && nextRune == ';' { + isEscaping = true + continue + } + + if isEscaping { + value.WriteRune(rune) + isEscaping = false + continue + } + + result.KeyValuePairs[key.String()] = value.String() + + key.Reset() + value.Reset() + isKey = true + case '=': + if isKey { + isKey = false + } else { + value.WriteRune(rune) + } + default: + if isKey { + key.WriteRune(rune) + } else { + value.WriteRune(rune) + } + } + } + + if isEscaping { + return result, NewInvalidConfigStrError("unescaped ';'") + } + + return result, nil +} diff --git a/conf_test.go b/conf_test.go new file mode 100644 index 0000000..98e769c --- /dev/null +++ b/conf_test.go @@ -0,0 +1,465 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "fmt" + "testing" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" + "github.com/stretchr/testify/assert" +) + +type parseConfigTestCase struct { + name string + config string + expected qdb.ConfigData + expectedErrMsgContains string +} + +func TestParserHappyCases(t *testing.T) { + var ( + addr = "localhost:1111" + user = "test-user" + pass = "test-pass" + token = "test-token" + min_throughput = 999 + request_timeout = time.Second * 88 + retry_timeout = time.Second * 99 + ) + + testCases := []parseConfigTestCase{ + { + name: "http and ipv4 address", + config: fmt.Sprintf("http::addr=%s", addr), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + }, + }, + }, + { + name: "http and ipv6 address", + config: "http::addr=::1;", + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": "::1", + }, + }, + }, + { + name: "tcp and address", + config: fmt.Sprintf("tcp::addr=%s", addr), + expected: qdb.ConfigData{ + Schema: "tcp", + KeyValuePairs: map[string]string{ + "addr": addr, + }, + }, + }, + { + name: "http and username/password", + config: fmt.Sprintf("http::addr=%s;username=%s;password=%s", addr, user, pass), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "username": user, + "password": pass, + }, + }, + }, + { + name: "http and token (with trailing ';')", + config: fmt.Sprintf("http::addr=%s;token=%s;", addr, token), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "token": token, + }, + }, + }, + { + name: "tcp with key and user", + config: fmt.Sprintf("tcp::addr=%s;token=%s;username=%s", addr, token, user), + expected: qdb.ConfigData{ + Schema: "tcp", + KeyValuePairs: map[string]string{ + "addr": addr, + "username": user, + "token": token, + }, + }, + }, + { + name: "https with min_throughput", + config: fmt.Sprintf("https::addr=%s;min_throughput=%d", addr, min_throughput), + expected: qdb.ConfigData{ + Schema: "https", + KeyValuePairs: map[string]string{ + "addr": addr, + "min_throughput": fmt.Sprintf("%d", min_throughput), + }, + }, + }, + { + name: "https with min_throughput, init_buf_size and tls_verify=unsafe_off", + config: fmt.Sprintf("https::addr=%s;min_throughput=%d;init_buf_size=%d;tls_verify=unsafe_off", addr, min_throughput, 1024), + expected: qdb.ConfigData{ + Schema: "https", + KeyValuePairs: map[string]string{ + "addr": addr, + "min_throughput": fmt.Sprintf("%d", min_throughput), + "init_buf_size": "1024", + "tls_verify": "unsafe_off", + }, + }, + }, + { + name: "tcps with tls_verify=unsafe_off", + config: fmt.Sprintf("tcps::addr=%s;tls_verify=unsafe_off", addr), + expected: qdb.ConfigData{ + Schema: "tcps", + KeyValuePairs: map[string]string{ + "addr": addr, + "tls_verify": "unsafe_off", + }, + }, + }, + { + name: "http with min_throughput, request_timeout, and retry_timeout", + config: fmt.Sprintf("http::addr=%s;min_throughput=%d;request_timeout=%d;retry_timeout=%d", + addr, min_throughput, request_timeout.Milliseconds(), retry_timeout.Milliseconds()), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "min_throughput": fmt.Sprintf("%d", min_throughput), + "request_timeout": fmt.Sprintf("%d", request_timeout.Milliseconds()), + "retry_timeout": fmt.Sprintf("%d", retry_timeout.Milliseconds()), + }, + }, + }, + { + name: "tcp with tls_verify=on", + config: fmt.Sprintf("tcp::addr=%s;tls_verify=on", addr), + expected: qdb.ConfigData{ + Schema: "tcp", + KeyValuePairs: map[string]string{ + "addr": addr, + "tls_verify": "on", + }, + }, + }, + { + name: "password with an escaped semicolon", + config: fmt.Sprintf("http::addr=%s;username=%s;password=pass;;word", addr, user), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "username": user, + "password": "pass;word", + }, + }, + }, + { + name: "password with an escaped semicolon (ending with a ';')", + config: fmt.Sprintf("http::addr=%s;username=%s;password=pass;;word;", addr, user), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "username": user, + "password": "pass;word", + }, + }, + }, + { + name: "password with a trailing semicolon", + config: fmt.Sprintf("http::addr=%s;username=%s;password=password;;;", addr, user), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "username": user, + "password": "password;", + }, + }, + }, + { + name: "equal sign in password", + config: fmt.Sprintf("http::addr=%s;username=%s;password=pass=word", addr, user), + expected: qdb.ConfigData{ + Schema: "http", + KeyValuePairs: map[string]string{ + "addr": addr, + "username": user, + "password": "pass=word", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := qdb.ParseConfigStr(tc.config) + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestParserPathologicalCases(t *testing.T) { + testCases := []parseConfigTestCase{ + { + name: "empty config", + config: "", + expectedErrMsgContains: "no schema separator found", + }, + { + name: "no schema", + config: "addr=localhost:9000", + expectedErrMsgContains: "no schema separator found", + }, + { + name: "no address", + config: "http::", + expectedErrMsgContains: "'addr' key not found", + }, + { + name: "unescaped semicolon in password leads to unexpected end of string", + config: "http::addr=localhost:9000;username=test;password=pass;word", + expectedErrMsgContains: "unexpected end of string", + }, + { + name: "unescaped semicolon in password leads to invalid key character", + config: "http::addr=localhost:9000;username=test;password=pass;word;", + expectedErrMsgContains: "invalid key character ';'", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := qdb.ParseConfigStr(tc.config) + var expected *qdb.InvalidConfigStrError + assert.Error(t, err) + assert.ErrorAs(t, err, &expected) + assert.Contains(t, err.Error(), tc.expectedErrMsgContains) + }) + } +} + +type configTestCase struct { + name string + config string + expectedOpts []qdb.LineSenderOption + expectedErrMsgContains string +} + +func TestHappyCasesFromConf(t *testing.T) { + var ( + addr = "localhost:1111" + user = "test-user" + pass = "test-pass" + token = "test-token" + minThroughput = 999 + requestTimeout = time.Second * 88 + retryTimeout = time.Second * 99 + initBufSize = 256 + maxBufSize = 1024 + ) + + testCases := []configTestCase{ + { + name: "user and token", + config: fmt.Sprintf("tcp::addr=%s;username=%s;token=%s", + addr, user, token), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithTcp(), + qdb.WithAddress(addr), + qdb.WithAuth(user, token), + }, + }, + { + name: "init_buf_size and max_buf_size", + config: fmt.Sprintf("tcp::addr=%s;init_buf_size=%d;max_buf_size=%d", + addr, initBufSize, maxBufSize), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithTcp(), + qdb.WithAddress(addr), + qdb.WithInitBufferSize(initBufSize), + qdb.WithMaxBufferSize(maxBufSize), + }, + }, + { + name: "with tls", + config: fmt.Sprintf("tcp::addr=%s;tls_verify=on", + addr), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithTcp(), + qdb.WithAddress(addr), + qdb.WithTls(), + }, + }, + { + name: "with tls and unsafe_off", + config: fmt.Sprintf("tcp::addr=%s;tls_verify=unsafe_off", + addr), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithTcp(), + qdb.WithAddress(addr), + qdb.WithTlsInsecureSkipVerify(), + }, + }, + { + name: "request_timeout and retry_timeout milli conversion", + config: fmt.Sprintf("http::addr=%s;request_timeout=%d;retry_timeout=%d", + addr, requestTimeout.Milliseconds(), retryTimeout.Milliseconds()), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithHttp(), + qdb.WithAddress(addr), + qdb.WithRequestTimeout(requestTimeout), + qdb.WithRetryTimeout(retryTimeout), + }, + }, + { + name: "password before username", + config: fmt.Sprintf("http::addr=%s;password=%s;username=%s", + addr, pass, user), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithHttp(), + qdb.WithAddress(addr), + qdb.WithBasicAuth(user, pass), + }, + }, + { + name: "min_throughput", + config: fmt.Sprintf("http::addr=%s;min_throughput=%d", + addr, minThroughput), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithHttp(), + qdb.WithAddress(addr), + qdb.WithMinThroughput(minThroughput), + }, + }, + { + name: "bearer token", + config: fmt.Sprintf("http::addr=%s;token=%s", + addr, token), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithHttp(), + qdb.WithAddress(addr), + qdb.WithBearerToken(token), + }, + }, + { + name: "auto flush", + config: fmt.Sprintf("http::addr=%s;auto_flush_rows=100;auto_flush_interval=1000", + addr), + expectedOpts: []qdb.LineSenderOption{ + qdb.WithHttp(), + qdb.WithAddress(addr), + qdb.WithAutoFlushRows(100), + qdb.WithAutoFlushInterval(1000), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := qdb.ConfFromStr(tc.config) + assert.NoError(t, err) + + expected := &qdb.LineSenderConfig{} + for _, opt := range tc.expectedOpts { + opt(expected) + } + + assert.Equal(t, expected, actual) + }) + } +} + +func TestPathologicalCasesFromConf(t *testing.T) { + testCases := []configTestCase{ + { + name: "empty config", + config: "", + expectedErrMsgContains: "no schema separator found", + }, + { + name: "invalid schema", + config: "foobar::addr=localhost:1111", + expectedErrMsgContains: "invalid schema", + }, + { + name: "invalid tls_verify 1", + config: "tcp::addr=localhost:1111;tls_verify=invalid", + expectedErrMsgContains: "invalid tls_verify", + }, + { + name: "invalid tls_verify 2", + config: "http::addr=localhost:1111;tls_verify=invalid", + expectedErrMsgContains: "invalid tls_verify", + }, + { + name: "unsupported option", + config: "tcp::addr=localhost:1111;unsupported_option=invalid", + expectedErrMsgContains: "unsupported option", + }, + { + name: "invalid auto_flush", + config: "http::addr=localhost:1111;auto_flush=invalid", + expectedErrMsgContains: "invalid auto_flush", + }, + { + name: "invalid auto_flush_rows", + config: "http::addr=localhost:1111;auto_flush_rows=invalid", + expectedErrMsgContains: "invalid auto_flush_rows", + }, + { + name: "invalid auto_flush_interval", + config: "http::addr=localhost:1111;auto_flush_interval=invalid", + expectedErrMsgContains: "invalid auto_flush_interval", + }, + { + name: "unsupported option", + config: "http::addr=localhost:1111;unsupported_option=invalid", + expectedErrMsgContains: "unsupported option", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := qdb.ConfFromStr(tc.config) + assert.ErrorContains(t, err, tc.expectedErrMsgContains) + }) + } +} diff --git a/examples.manifest.yaml b/examples.manifest.yaml index 275a8dd..94bd1cf 100644 --- a/examples.manifest.yaml +++ b/examples.manifest.yaml @@ -2,15 +2,15 @@ # substitute host/port and auth keys when auto-generating examples - name: ilp lang: go - path: examples/basic/main.go + path: examples/tcp/basic/main.go header: |- - Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v2) + Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v3) and [repo](https://github.com/questdb/go-questdb-client). - name: ilp-auth lang: go - path: examples/auth/main.go + path: examples/tcp/auth/main.go header: |- - Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v2) + Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v3) and [repo](https://github.com/questdb/go-questdb-client). auth: kid: testUser1 @@ -20,9 +20,9 @@ port: 9009 - name: ilp-auth-tls lang: go - path: examples/auth-and-tls/main.go + path: examples/tcp/auth-and-tls/main.go header: |- - Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v2) + Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v3) and [repo](https://github.com/questdb/go-questdb-client). auth: kid: testUser1 @@ -30,3 +30,10 @@ addr: host: localhost port: 9009 +- name: ilp-from-conf + lang: go + path: examples/http/from-conf/main.go + header: |- + Go client library [docs](https://pkg.go.dev/github.com/questdb/go-questdb-client/v3) + and [repo](https://github.com/questdb/go-questdb-client). + conf: http::addr=localhost:9000; diff --git a/examples/from-conf/main.go b/examples/from-conf/main.go new file mode 100644 index 0000000..31c6f0b --- /dev/null +++ b/examples/from-conf/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "log" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" +) + +func main() { + ctx := context.TODO() + sender, err := qdb.LineSenderFromConf(ctx, "http::addr=localhost:9000;") + if err != nil { + log.Fatal(err) + } + // Make sure to close the sender on exit to release resources. + defer func() { + err := sender.Close(ctx) + if err != nil { + log.Fatal(err) + } + }() + + // Send a few ILP messages. + bday, err := time.Parse(time.DateOnly, "1856-07-10") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "Austrian Empire"). + TimestampColumn("birthdate", bday). // Epoch in micros. + Int64Column("id", 0). + StringColumn("name", "Nicola Tesla"). + At(ctx, time.Now()) // Epoch in nanos. + if err != nil { + log.Fatal(err) + } + + bday, err = time.Parse(time.DateOnly, "1847-02-11") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "USA"). + TimestampColumn("birthdate", bday). + Int64Column("id", 1). + StringColumn("name", "Thomas Alva Edison"). + AtNow(ctx) + if err != nil { + log.Fatal(err) + } + + // Make sure that the messages are sent over the network. + err = sender.Flush(ctx) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/http/auth-and-tls/main.go b/examples/http/auth-and-tls/main.go new file mode 100644 index 0000000..4cc2679 --- /dev/null +++ b/examples/http/auth-and-tls/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "log" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" +) + +func main() { + ctx := context.TODO() + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress("localhost:9000"), + qdb.WithBearerToken( + "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48", // token here + ), + qdb.WithTls(), + ) + if err != nil { + log.Fatal(err) + } + // Make sure to close the sender on exit to release resources. + defer func() { + err := sender.Close(ctx) + if err != nil { + log.Fatal(err) + } + }() + + // Send a few ILP messages. + bday, err := time.Parse(time.DateOnly, "1856-07-10") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "Austrian Empire"). + TimestampColumn("birthdate", bday). // Epoch in micros. + Int64Column("id", 0). + StringColumn("name", "Nicola Tesla"). + At(ctx, time.Now()) // Epoch in nanos. + if err != nil { + log.Fatal(err) + } + + bday, err = time.Parse(time.DateOnly, "1847-02-11") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "USA"). + TimestampColumn("birthdate", bday). + Int64Column("id", 1). + StringColumn("name", "Thomas Alva Edison"). + AtNow(ctx) + if err != nil { + log.Fatal(err) + } + + // Make sure that the messages are sent over the network. + err = sender.Flush(ctx) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/http/auth/main.go b/examples/http/auth/main.go new file mode 100644 index 0000000..7092768 --- /dev/null +++ b/examples/http/auth/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "log" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" +) + +func main() { + ctx := context.TODO() + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress("localhost:9000"), + qdb.WithBasicAuth( + "testUser1", // username + "testPassword1", // password + ), + ) + if err != nil { + log.Fatal(err) + } + // Make sure to close the sender on exit to release resources. + defer func() { + err := sender.Close(ctx) + if err != nil { + log.Fatal(err) + } + }() + + // Send a few ILP messages. + bday, err := time.Parse(time.DateOnly, "1856-07-10") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "Austrian Empire"). + TimestampColumn("birthdate", bday). // Epoch in micros. + Int64Column("id", 0). + StringColumn("name", "Nicola Tesla"). + At(ctx, time.Now()) // Epoch in nanos. + if err != nil { + log.Fatal(err) + } + + bday, err = time.Parse(time.DateOnly, "1847-02-11") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "USA"). + TimestampColumn("birthdate", bday). + Int64Column("id", 1). + StringColumn("name", "Thomas Alva Edison"). + AtNow(ctx) + if err != nil { + log.Fatal(err) + } + + // Make sure that the messages are sent over the network. + err = sender.Flush(ctx) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/http/basic/main.go b/examples/http/basic/main.go new file mode 100644 index 0000000..9852a2e --- /dev/null +++ b/examples/http/basic/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "log" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" +) + +func main() { + ctx := context.TODO() + // Connect to QuestDB running on 127.0.0.1:9009 + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp()) + if err != nil { + log.Fatal(err) + } + // Make sure to close the sender on exit to release resources. + defer func() { + err := sender.Close(ctx) + if err != nil { + log.Fatal(err) + } + }() + + // Send a few ILP messages. + bday, err := time.Parse(time.DateOnly, "1856-07-10") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "Austrian Empire"). + TimestampColumn("birthdate", bday). // Epoch in micros. + Int64Column("id", 0). + StringColumn("name", "Nicola Tesla"). + At(ctx, time.Now()) // Epoch in nanos. + if err != nil { + log.Fatal(err) + } + + bday, err = time.Parse(time.DateOnly, "1847-02-11") + if err != nil { + log.Fatal(err) + } + err = sender. + Table("inventors"). + Symbol("born", "USA"). + TimestampColumn("birthdate", bday). + Int64Column("id", 1). + StringColumn("name", "Thomas Alva Edison"). + AtNow(ctx) + if err != nil { + log.Fatal(err) + } + + // Make sure that the messages are sent over the network. + err = sender.Flush(ctx) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/auth-and-tls/main.go b/examples/tcp/auth-and-tls/main.go similarity index 94% rename from examples/auth-and-tls/main.go rename to examples/tcp/auth-and-tls/main.go index 27a77d1..06a1e09 100644 --- a/examples/auth-and-tls/main.go +++ b/examples/tcp/auth-and-tls/main.go @@ -5,7 +5,7 @@ import ( "log" "time" - qdb "github.com/questdb/go-questdb-client/v2" + qdb "github.com/questdb/go-questdb-client/v3" ) func main() { @@ -23,7 +23,7 @@ func main() { log.Fatal(err) } // Make sure to close the sender on exit to release resources. - defer sender.Close() + defer sender.Close(ctx) // Send a few ILP messages. bday, err := time.Parse(time.DateOnly, "1856-07-10") diff --git a/examples/auth/main.go b/examples/tcp/auth/main.go similarity index 94% rename from examples/auth/main.go rename to examples/tcp/auth/main.go index 6545b44..bd86a83 100644 --- a/examples/auth/main.go +++ b/examples/tcp/auth/main.go @@ -5,7 +5,7 @@ import ( "log" "time" - qdb "github.com/questdb/go-questdb-client/v2" + qdb "github.com/questdb/go-questdb-client/v3" ) func main() { @@ -22,7 +22,7 @@ func main() { log.Fatal(err) } // Make sure to close the sender on exit to release resources. - defer sender.Close() + defer sender.Close(ctx) // Send a few ILP messages. bday, err := time.Parse(time.DateOnly, "1856-07-10") diff --git a/examples/basic/main.go b/examples/tcp/basic/main.go similarity index 93% rename from examples/basic/main.go rename to examples/tcp/basic/main.go index 2f799a0..8c6c8c6 100644 --- a/examples/basic/main.go +++ b/examples/tcp/basic/main.go @@ -5,7 +5,7 @@ import ( "log" "time" - qdb "github.com/questdb/go-questdb-client/v2" + qdb "github.com/questdb/go-questdb-client/v3" ) func main() { @@ -16,7 +16,7 @@ func main() { log.Fatal(err) } // Make sure to close the sender on exit to release resources. - defer sender.Close() + defer sender.Close(ctx) // Send a few ILP messages. bday, err := time.Parse(time.DateOnly, "1856-07-10") diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..0abfc2f --- /dev/null +++ b/export_test.go @@ -0,0 +1,78 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +type ( + Buffer = buffer + ConfigData = configData + TcpLineSender = tcpLineSender + LineSenderConfig = lineSenderConfig +) + +var ( + GlobalTransport = globalTransport +) + +func NewBuffer(initBufSize int, maxBufSize int, fileNameLimit int) Buffer { + return newBuffer(initBufSize, maxBufSize, fileNameLimit) +} + +func ParseConfigStr(conf string) (configData, error) { + return parseConfigStr(conf) +} + +func ConfFromStr(conf string) (*LineSenderConfig, error) { + return confFromStr(conf) +} + +func Messages(s LineSender) string { + if hs, ok := s.(*httpLineSender); ok { + return hs.Messages() + } + if ts, ok := s.(*tcpLineSender); ok { + return ts.Messages() + } + panic("unexpected struct") +} + +func MsgCount(s LineSender) int { + if hs, ok := s.(*httpLineSender); ok { + return hs.MsgCount() + } + if ts, ok := s.(*tcpLineSender); ok { + return ts.MsgCount() + } + panic("unexpected struct") +} + +func BufLen(s LineSender) int { + if hs, ok := s.(*httpLineSender); ok { + return hs.BufLen() + } + if ts, ok := s.(*tcpLineSender); ok { + return ts.BufLen() + } + panic("unexpected struct") +} diff --git a/go.mod b/go.mod index 1ad784f..7ee044f 100644 --- a/go.mod +++ b/go.mod @@ -1,63 +1,63 @@ -module github.com/questdb/go-questdb-client/v2 +module github.com/questdb/go-questdb-client/v3 -go 1.17 +go 1.18 require ( - github.com/stretchr/testify v1.8.4 - github.com/testcontainers/testcontainers-go v0.25.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.26.0 ) require ( dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.11.1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/containerd/cgroups v1.1.0 // indirect - github.com/containerd/containerd v1.7.6 // indirect + github.com/containerd/containerd v1.7.12 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v24.0.6+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/docker v25.0.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/mount v0.3.3 // indirect - github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect - github.com/opencontainers/runc v1.1.9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect - github.com/shirou/gopsutil/v3 v3.23.9 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/stretchr/objx v0.5.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.16.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/tools v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect - google.golang.org/grpc v1.58.2 // indirect + google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 66b6c35..da8dd5b 100644 --- a/go.sum +++ b/go.sum @@ -1,568 +1,82 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w= -github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.23 h1:47MSwtKGXet80aIn+7h4YI6fwPmwIghAnsx2aOUrG2M= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA= -github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= -github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.9 h1:rs6Xg1gtIxaeyG+Smsb/0xaSDu1VgFhOCKBXxMxbsF4= -github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= -github.com/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8= -github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo= -github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= -github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY= +github.com/docker/docker v25.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM= -github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= -github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= -github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= -github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= -github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runc v1.0.2 h1:opHZMaswlyxz1OuGpBE53Dwe4/xF7EZTY0A2L/FpCOg= -github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runc v1.1.9 h1:XR0VIHTGce5eWPkaPesqTBrhW2yAcaraWfsEalNwQLM= -github.com/opencontainers/runc v1.1.9/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -570,534 +84,104 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/shirou/gopsutil/v3 v3.23.9 h1:ZI5bWVeu2ep4/DIxB4U9okeYJ7zp/QLTO4auRb/ty/E= -github.com/shirou/gopsutil/v3 v3.23.9/go.mod h1:x/NWSb71eMcjFIO0vhyGW5nZ7oSIgVjrCnADckb85GA= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/testcontainers/testcontainers-go v0.13.0 h1:OUujSlEGsXVo/ykPVZk3KanBNGN0TYb/7oKIPVn15JA= -github.com/testcontainers/testcontainers-go v0.13.0/go.mod h1:z1abufU633Eb/FmSBTzV6ntZAC1eZBYPtaFsn4nPuDk= -github.com/testcontainers/testcontainers-go v0.25.0 h1:erH6cQjsaJrH+rJDU9qIf89KFdhK0Bft0aEZHlYC3Vs= -github.com/testcontainers/testcontainers-go v0.25.0/go.mod h1:4sC9SiJyzD1XFi59q8umTQYWxnkweEc5OjVtTUlJzqQ= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.26.0 h1:uqcYdoOHBy1ca7gKODfBd9uTHVK3a7UL848z09MVZ0c= +github.com/testcontainers/testcontainers-go v0.26.0/go.mod h1:ICriE9bLX5CLxL9OFQ2N+2N+f+803LNJ1utJb1+Inx0= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 h1:9k5exFQKQglLo+RoP+4zMjOFE14P6+vyR0baDAi0Rcs= golang.org/x/exp v0.0.0-20231005195138-3e424a577f31/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211108170745-6635138e15ea h1:FosBMXtOc8Tp9Hbo4ltl1WJSrTVewZU8MPnTPY2HdH8= -golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= -google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= -google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/gotestsum v1.7.0/go.mod h1:V1m4Jw3eBerhI/A6qCxUE07RnCg7ACkKj9BYcAm09V8= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= diff --git a/http_errors.go b/http_errors.go new file mode 100644 index 0000000..41afcc4 --- /dev/null +++ b/http_errors.go @@ -0,0 +1,79 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +import ( + "fmt" + "time" +) + +// HttpError is a server-sent error message. +type HttpError struct { + httpStatus int + + Code string `json:"code"` + Message string `json:"message"` + Line int `json:"line,omitempty"` + ErrorId string `json:"errorId"` +} + +// Error returns full error message string. +func (e *HttpError) Error() string { + return fmt.Sprintf("%d %s id: %s, code: %s, line: %d", + e.httpStatus, + e.Message, + e.ErrorId, + e.Code, + e.Line, + ) +} + +// HttpStatus returns error HTTP status code. +func (e *HttpError) HttpStatus() int { + return e.httpStatus +} + +// RetryTimeoutError is error indicating failed flush retry attempt. +type RetryTimeoutError struct { + LastErr error + Timeout time.Duration +} + +// NewRetryTimeoutError returns a new RetryTimeoutError error. +func NewRetryTimeoutError(timeout time.Duration, lastError error) *RetryTimeoutError { + return &RetryTimeoutError{ + LastErr: lastError, + Timeout: timeout, + } +} + +// Error returns full error message string. +func (e *RetryTimeoutError) Error() string { + msg := fmt.Sprintf("retry timeout reached: %s.", e.Timeout) + if e.LastErr != nil { + msg += " " + e.LastErr.Error() + } + return msg +} diff --git a/http_integration_test.go b/http_integration_test.go new file mode 100644 index 0000000..246e6cb --- /dev/null +++ b/http_integration_test.go @@ -0,0 +1,124 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "context" + "reflect" + "testing" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" + "github.com/stretchr/testify/assert" +) + +func (suite *integrationTestSuite) TestE2ESuccessfulHttpBasicAuthWithTlsProxy() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + questdbC, err := setupQuestDB(ctx, httpBasicAuth) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(questdbC.proxyIlpHttpAddress), + qdb.WithBasicAuth(basicAuthUser, basicAuthPass), + qdb.WithTlsInsecureSkipVerify(), + ) + assert.NoError(suite.T(), err) + defer sender.Close(ctx) + + err = sender. + Table(testTable). + StringColumn("str_col", "foobar"). + At(ctx, time.UnixMicro(1)) + assert.NoError(suite.T(), err) + + err = sender. + Table(testTable). + StringColumn("str_col", "barbaz"). + At(ctx, time.UnixMicro(2)) + assert.NoError(suite.T(), err) + + err = sender.Flush(ctx) + assert.NoError(suite.T(), err) + + expected := tableData{ + Columns: []column{ + {"str_col", "STRING"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"foobar", "1970-01-01T00:00:00.000001Z"}, + {"barbaz", "1970-01-01T00:00:00.000002Z"}, + }, + Count: 2, + } + + assert.Eventually(suite.T(), func() bool { + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + return reflect.DeepEqual(expected, data) + }, eventualDataTimeout, 100*time.Millisecond) +} + +func (suite *integrationTestSuite) TestServerSideError() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + var ( + sender qdb.LineSender + err error + ) + + questdbC, err := setupQuestDB(ctx, noAuth) + assert.NoError(suite.T(), err) + + sender, err = qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(questdbC.httpAddress)) + assert.NoError(suite.T(), err) + + err = sender.Table(testTable).Int64Column("long_col", 42).AtNow(ctx) + assert.NoError(suite.T(), err) + err = sender.Flush(ctx) + assert.NoError(suite.T(), err) + + // Now, use wrong type for the long_col. + err = sender.Table(testTable).StringColumn("long_col", "42").AtNow(ctx) + assert.NoError(suite.T(), err) + err = sender.Flush(ctx) + assert.Error(suite.T(), err) + assert.ErrorContains(suite.T(), err, "my_test_table, column: long_col; cast error from protocol type: STRING to column type") + assert.ErrorContains(suite.T(), err, "line: 1") + + sender.Close(ctx) + questdbC.Stop(ctx) +} diff --git a/http_sender.go b/http_sender.go new file mode 100644 index 0000000..ef48f64 --- /dev/null +++ b/http_sender.go @@ -0,0 +1,434 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "math/rand" + "net/http" + "sync/atomic" + "time" +) + +type globalHttpTransport struct { + transport *http.Transport + // clientCt is used to track the number of open httpLineSenders + // If the clientCt reaches 0, meaning all senders have been + // closed, the global transport closes all idle connections to + // free up resources + clientCt atomic.Int64 +} + +func (t *globalHttpTransport) ClientCount() int64 { + return t.clientCt.Load() +} + +func (t *globalHttpTransport) RegisterClient() { + t.clientCt.Add(1) +} + +func (t *globalHttpTransport) UnregisterClient() { + newCt := t.clientCt.Add(-1) + if newCt == 0 { + t.transport.CloseIdleConnections() + } +} + +var ( + // We use a shared http transport to pool connections + // across HttpLineSenders + globalTransport *globalHttpTransport = &globalHttpTransport{transport: newHttpTransport()} +) + +func newHttpTransport() *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + MaxConnsPerHost: 0, + MaxIdleConns: 64, + MaxIdleConnsPerHost: 64, + IdleConnTimeout: 120 * time.Second, + TLSHandshakeTimeout: defaultRequestTimeout, + TLSClientConfig: &tls.Config{}, + } +} + +// HttpLineSender allows you to insert rows into QuestDB by sending ILP +// messages over HTTP(S). +// +// Each sender corresponds to a single HTTP client. All senders +// utilize a global transport for connection pooling. A sender +// should not be called concurrently by multiple goroutines. +type httpLineSender struct { + buf buffer + + address string + + // Retry/timeout-related fields + retryTimeout time.Duration + minThroughputBytesPerSecond int + requestTimeout time.Duration + + // Auto-flush fields + autoFlushRows int + autoFlushInterval time.Duration + flushDeadline time.Time + + // Authentication-related fields + user string + pass string + token string + + client http.Client + uri string + closed bool + + // Global transport is used unless a custom transport was provided. + globalTransport *globalHttpTransport +} + +func newHttpLineSender(conf *lineSenderConfig) (*httpLineSender, error) { + var transport *http.Transport + + s := &httpLineSender{ + address: conf.address, + minThroughputBytesPerSecond: conf.minThroughput, + requestTimeout: conf.requestTimeout, + retryTimeout: conf.retryTimeout, + autoFlushRows: conf.autoFlushRows, + autoFlushInterval: conf.autoFlushInterval, + user: conf.httpUser, + pass: conf.httpPass, + token: conf.httpToken, + + buf: newBuffer(conf.initBufSize, conf.maxBufSize, conf.fileNameLimit), + } + + if conf.httpTransport != nil { + // Use custom transport. + transport = conf.httpTransport + } else if conf.tlsMode == tlsInsecureSkipVerify { + // We can't use the global transport in case of skipped TLS verification. + // Instead, create a single-time transport with disabled keep-alives. + transport = newHttpTransport() + transport.DisableKeepAlives = true + transport.TLSClientConfig.InsecureSkipVerify = true + } else { + // Otherwise, use the global transport. + s.globalTransport = globalTransport + transport = globalTransport.transport + } + + s.client = http.Client{ + Transport: transport, + Timeout: 0, + } + + if s.globalTransport != nil { + s.globalTransport.RegisterClient() + } + + s.uri = "http" + if conf.tlsMode != tlsDisabled { + s.uri += "s" + } + s.uri += fmt.Sprintf("://%s/write", s.address) + + return s, nil +} + +func (s *httpLineSender) Flush(ctx context.Context) error { + return s.flush0(ctx, false) +} + +func (s *httpLineSender) flush0(ctx context.Context, closing bool) error { + var ( + req *http.Request + retryInterval time.Duration + + maxRetryInterval = time.Second + ) + + if s.closed { + return errors.New("cannot flush a closed LineSender") + } + + err := s.buf.LastErr() + s.buf.ClearLastErr() + if err != nil { + s.buf.DiscardPendingMsg() + return err + } + if s.buf.HasTable() { + s.buf.DiscardPendingMsg() + return errors.New("pending ILP message must be finalized with At or AtNow before calling Flush") + } + + if s.buf.msgCount == 0 { + return nil + } + + // We rely on the following HTTP client implicit behavior: + // s.buf implements WriteTo method which is used by the client. + req, err = http.NewRequest( + http.MethodPost, + s.uri, + &s.buf, + ) + if err != nil { + return err + } + + if s.user != "" && s.pass != "" { + req.SetBasicAuth(s.user, s.pass) + } else if s.token != "" { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.token)) + } + + retry, err := s.makeRequest(ctx, req) + if !retry { + s.refreshFlushDeadline(err) + return err + } + + if !closing && s.retryTimeout > 0 { + retryStartTime := time.Now() + + retryInterval = 10 * time.Millisecond + for err != nil { + if time.Since(retryStartTime) > s.retryTimeout { + return NewRetryTimeoutError(s.retryTimeout, err) + } + + jitter := time.Duration(rand.Intn(10)) * time.Millisecond + time.Sleep(retryInterval + jitter) + + retry, err = s.makeRequest(ctx, req) + if !retry { + s.refreshFlushDeadline(err) + return err + } + + // Retry with exponentially-increasing timeout + // up to a global maximum (1 second) + retryInterval = retryInterval * 2 + if retryInterval > maxRetryInterval { + retryInterval = maxRetryInterval + } + } + } + + s.refreshFlushDeadline(err) + return err +} + +func (s *httpLineSender) refreshFlushDeadline(err error) { + if s.autoFlushInterval > 0 { + if err != nil { + s.flushDeadline = time.Time{} + } else { + s.flushDeadline = time.Now().Add(s.autoFlushInterval) + } + } +} + +func (s *httpLineSender) Table(name string) LineSender { + s.buf.Table(name) + return s +} + +func (s *httpLineSender) Symbol(name, val string) LineSender { + s.buf.Symbol(name, val) + return s +} + +func (s *httpLineSender) Int64Column(name string, val int64) LineSender { + s.buf.Int64Column(name, val) + return s +} + +func (s *httpLineSender) Long256Column(name string, val *big.Int) LineSender { + s.buf.Long256Column(name, val) + return s +} + +func (s *httpLineSender) TimestampColumn(name string, ts time.Time) LineSender { + s.buf.TimestampColumn(name, ts) + return s +} + +func (s *httpLineSender) Float64Column(name string, val float64) LineSender { + s.buf.Float64Column(name, val) + return s +} + +func (s *httpLineSender) StringColumn(name, val string) LineSender { + s.buf.StringColumn(name, val) + return s +} + +func (s *httpLineSender) BoolColumn(name string, val bool) LineSender { + s.buf.BoolColumn(name, val) + return s +} + +func (s *httpLineSender) Close(ctx context.Context) error { + if s.closed { + return nil + } + + var err error + + if s.autoFlushRows > 0 { + err = s.flush0(ctx, true) + } + + s.closed = true + + if s.globalTransport != nil { + s.globalTransport.UnregisterClient() + } + + return err +} + +func (s *httpLineSender) AtNow(ctx context.Context) error { + return s.At(ctx, time.Time{}) +} + +func (s *httpLineSender) At(ctx context.Context, ts time.Time) error { + if s.closed { + return errors.New("cannot queue new messages on a closed LineSender") + } + + sendTs := true + if ts.IsZero() { + sendTs = false + } + err := s.buf.At(ts, sendTs) + if err != nil { + return err + } + + // Check row count-based auto flush. + if s.buf.msgCount == s.autoFlushRows { + return s.Flush(ctx) + } + // Check time-based auto flush. + if s.autoFlushInterval > 0 { + if s.flushDeadline.IsZero() { + s.flushDeadline = time.Now().Add(s.autoFlushInterval) + } else if time.Now().After(s.flushDeadline) { + return s.Flush(ctx) + } + } + + return nil +} + +// makeRequest returns a boolean if we need to retry the request +func (s *httpLineSender) makeRequest(ctx context.Context, req *http.Request) (bool, error) { + // reqTimeout = ( request.len() / min_throughput ) + request_timeout + // nb: conversion from int to time.Duration is in milliseconds + reqTimeout := time.Duration(s.buf.Len()/s.minThroughputBytesPerSecond)*time.Second + s.requestTimeout + reqCtx, cancel := context.WithTimeout(ctx, reqTimeout) + defer cancel() + + req = req.WithContext(reqCtx) + resp, err := s.client.Do(req) + if err != nil { + return true, err + } + + defer resp.Body.Close() + + // Don't retry on successful responses + if resp.StatusCode < 300 { + return false, nil + } + + // Retry on known 500-related errors + if isRetryableError(resp.StatusCode) { + return true, fmt.Errorf("%d: %s", resp.StatusCode, resp.Status) + } + + // For all other response codes, attempt to parse the body + // as a JSON error message from the QuestDB server. + // If this fails at any point, just return the status message + // and body contents (if any) + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("%d: %s", resp.StatusCode, resp.Status) + } + httpErr := &HttpError{ + httpStatus: resp.StatusCode, + } + err = json.Unmarshal(buf, httpErr) + if err != nil { + return false, fmt.Errorf("%d: %s -- %s", resp.StatusCode, resp.Status, buf) + } + + return false, httpErr + +} + +func isRetryableError(statusCode int) bool { + switch statusCode { + case 500, // Internal Server Error + 503, // Service Unavailable + 504, // Gateway Timeout + 507, // Insufficient Storage + 509, // Bandwidth Limit Exceeded + 523, // Origin is Unreachable + 524, // A Timeout Occurred + 529, // Site is overloaded + 599: // Network Connect Timeout Error + return true + default: + return false + } +} + +// Messages returns a copy of accumulated ILP messages that are not +// flushed to the TCP connection yet. Useful for debugging purposes. +func (s *httpLineSender) Messages() string { + return s.buf.Messages() +} + +// MsgCount returns the number of buffered messages +func (s *httpLineSender) MsgCount() int { + return s.buf.msgCount +} + +// BufLen returns the number of bytes written to the buffer. +func (s *httpLineSender) BufLen() int { + return s.buf.Len() +} diff --git a/http_sender_test.go b/http_sender_test.go new file mode 100644 index 0000000..e404ede --- /dev/null +++ b/http_sender_test.go @@ -0,0 +1,623 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "testing" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" + "github.com/stretchr/testify/assert" +) + +type httpConfigTestCase struct { + name string + config string + expectedErr string +} + +func TestHttpHappyCasesFromConf(t *testing.T) { + var ( + addr = "localhost:1111" + user = "test-user" + pass = "test-pass" + token = "test-token" + min_throughput = 999 + request_timeout = time.Second * 88 + retry_timeout = time.Second * 99 + ) + + testCases := []httpConfigTestCase{ + { + name: "request_timeout and retry_timeout milli conversion", + config: fmt.Sprintf("http::addr=%s;request_timeout=%d;retry_timeout=%d", + addr, request_timeout.Milliseconds(), retry_timeout.Milliseconds()), + }, + { + name: "pass before user", + config: fmt.Sprintf("http::addr=%s;password=%s;username=%s", + addr, pass, user), + }, + { + name: "min_throughput", + config: fmt.Sprintf("http::addr=%s;min_throughput=%d", + addr, min_throughput), + }, + { + name: "bearer token", + config: fmt.Sprintf("http::addr=%s;token=%s", + addr, token), + }, + { + name: "auto flush", + config: fmt.Sprintf("http::addr=%s;auto_flush_rows=100;auto_flush_interval=1000", + addr), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sender, err := qdb.LineSenderFromConf(context.Background(), tc.config) + assert.NoError(t, err) + + sender.Close(context.Background()) + }) + } +} + +func TestHttpPathologicalCasesFromConf(t *testing.T) { + testCases := []httpConfigTestCase{ + { + name: "basic_and_token_auth", + config: "http::username=test_user;token=test_token", + expectedErr: "both basic and token", + }, + { + name: "negative init_buf_size", + config: "tcp::init_buf_size=-1", + expectedErr: "initial buffer size is negative", + }, + { + name: "negative max_buf_size", + config: "tcp::max_buf_size=-1", + expectedErr: "max buffer size is negative", + }, + { + name: "negative retry timeout", + config: "tcp::retry_timeout=-1", + expectedErr: "retry timeout is negative", + }, + { + name: "negative request timeout", + config: "tcp::request_timeout=-1", + expectedErr: "request timeout is negative", + }, + { + name: "negative min throughput", + config: "tcp::min_throughput=-1", + expectedErr: "min throughput is negative", + }, + { + name: "negative auto flush rows", + config: "tcp::auto_flush_rows=-1", + expectedErr: "auto flush rows is negative", + }, + { + name: "negative auto flush interval", + config: "tcp::auto_flush_interval=-1", + expectedErr: "auto flush interval is negative", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := qdb.LineSenderFromConf(context.Background(), tc.config) + assert.ErrorContains(t, err, tc.expectedErr) + }) + } +} + +func TestErrorWhenSenderTypeIsNotSpecified(t *testing.T) { + ctx := context.Background() + + _, err := qdb.NewLineSender(ctx) + assert.Error(t, err) + assert.ErrorContains(t, err, "sender type is not specified: use WithHttp or WithTcp") +} + +func TestHttpErrorWhenMaxBufferSizeIsReached(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithInitBufferSize(4), + qdb.WithMaxBufferSize(8), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + err = sender.Table(testTable).Symbol("sym", "foobar").AtNow(ctx) + assert.Error(t, err) + assert.ErrorContains(t, err, "buffer size exceeded maximum limit") + assert.Empty(t, qdb.Messages(sender)) +} + +func TestHttpErrorOnFlushWhenMessageIsPending(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + sender.Table(testTable) + err = sender.Flush(ctx) + + assert.ErrorContains(t, err, "pending ILP message must be finalized with At or AtNow before calling Flush") + assert.Empty(t, qdb.Messages(sender)) +} + +func TestNoOpOnFlushWhenNoMessagesAreWritten(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + err = sender.Flush(ctx) + + assert.NoError(t, err) + assert.Empty(t, qdb.Messages(sender)) +} + +func TestErrorOnContextDeadlineHttp(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(50*time.Millisecond)) + defer cancel() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + // Keep writing until we get an error due to the context deadline. + for i := 0; i < 100_000; i++ { + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + if err != nil { + return + } + err = sender.Flush(ctx) + if err != nil { + return + } + time.Sleep(5 * time.Millisecond) + } + t.Fail() +} + +func TestRetryOn500(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(returning500) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithRequestTimeout(10*time.Millisecond), + qdb.WithRetryTimeout(50*time.Millisecond), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + if err != nil { + return + } + err = sender.Flush(ctx) + retryErr := &qdb.RetryTimeoutError{} + assert.ErrorAs(t, err, &retryErr) + assert.ErrorContains(t, retryErr.LastErr, "500") +} + +func TestNoRetryOn400FromProxy(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(returning403) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithRequestTimeout(10*time.Millisecond), + qdb.WithRetryTimeout(50*time.Millisecond), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + if err != nil { + return + } + err = sender.Flush(ctx) + retryTimeoutErr := &qdb.RetryTimeoutError{} + assert.False(t, errors.As(err, &retryTimeoutErr)) + assert.ErrorContains(t, err, "Forbidden") +} + +func TestNoRetryOn400FromServer(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(returning404) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithRequestTimeout(10*time.Millisecond), + qdb.WithRetryTimeout(50*time.Millisecond), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + if err != nil { + return + } + err = sender.Flush(ctx) + httpErr := &qdb.HttpError{} + assert.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusNotFound, httpErr.HttpStatus()) + assert.Equal(t, "Not Found", httpErr.Message) + assert.Equal(t, 42, httpErr.Line) +} + +func TestRowBasedAutoFlush(t *testing.T) { + ctx := context.Background() + autoFlushRows := 10 + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithAutoFlushRows(autoFlushRows), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + // Send autoFlushRows - 1 messages and ensure all are buffered + for i := 0; i < autoFlushRows-1; i++ { + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + assert.NoError(t, err) + } + + assert.Equal(t, autoFlushRows-1, qdb.MsgCount(sender)) + + // Send one additional message and ensure that all are flushed + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + assert.NoError(t, err) + + assert.Equal(t, 0, qdb.MsgCount(sender)) +} + +func TestTimeBasedAutoFlush(t *testing.T) { + ctx := context.Background() + autoFlushInterval := 10 * time.Millisecond + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithAutoFlushRows(1000), + qdb.WithAutoFlushInterval(autoFlushInterval), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + // Send a message and ensure it's buffered + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + assert.NoError(t, err) + assert.Equal(t, 1, qdb.MsgCount(sender)) + + time.Sleep(2 * autoFlushInterval) + + // Send one additional message and ensure that both messages are flushed + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + assert.NoError(t, err) + + assert.Equal(t, 0, qdb.MsgCount(sender)) +} + +func TestNoFlushWhenAutoFlushDisabled(t *testing.T) { + ctx := context.Background() + autoFlushRows := 10 + autoFlushInterval := time.Duration(autoFlushRows-1) * time.Millisecond + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + // opts are processed sequentially, so AutoFlushDisabled will + // override AutoFlushRows + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithAutoFlushRows(autoFlushRows), + qdb.WithAutoFlushInterval(autoFlushInterval), + qdb.WithAutoFlushDisabled(), + ) + assert.NoError(t, err) + defer sender.Close(ctx) + + // Send autoFlushRows + 1 messages and ensure all are buffered + for i := 0; i < autoFlushRows+1; i++ { + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + assert.NoError(t, err) + time.Sleep(time.Millisecond) + } + + assert.Equal(t, autoFlushRows+1, qdb.MsgCount(sender)) +} + +func TestSenderDoubleClose(t *testing.T) { + ctx := context.Background() + autoFlushRows := 10 + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + // opts are processed sequentially, so AutoFlushDisabled will + // override AutoFlushRows + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithAutoFlushRows(autoFlushRows), + qdb.WithAutoFlushDisabled(), + ) + assert.NoError(t, err) + + err = sender.Close(ctx) + assert.NoError(t, err) + + err = sender.Close(ctx) + assert.NoError(t, err) +} + +func TestErrorOnFlushWhenSenderIsClosed(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + err = sender.Close(ctx) + assert.NoError(t, err) + + sender.Table(testTable) + err = sender.Flush(ctx) + + assert.ErrorContains(t, err, "cannot flush a closed LineSender") +} + +func TestAutoFlushWhenSenderIsClosed(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + + err = sender.Table(testTable).Symbol("abc", "def").AtNow(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, qdb.Messages(sender)) + + err = sender.Close(ctx) + assert.NoError(t, err) + assert.Empty(t, qdb.Messages(sender)) +} + +func TestNoFlushWhenSenderIsClosedAndAutoFlushIsDisabled(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithAddress(srv.Addr()), + qdb.WithAutoFlushDisabled(), + ) + assert.NoError(t, err) + + err = sender.Table(testTable).Symbol("abc", "def").AtNow(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, qdb.Messages(sender)) + + err = sender.Close(ctx) + assert.NoError(t, err) + assert.Empty(t, qdb.Messages(sender)) +} + +func TestBufferClearAfterFlush(t *testing.T) { + ctx := context.Background() + + srv, err := newTestHttpServer(sendToBackChannel) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + err = sender.Table(testTable).Symbol("abc", "def").AtNow(ctx) + assert.NoError(t, err) + + err = sender.Flush(ctx) + assert.NoError(t, err) + + expectLines(t, srv.BackCh, []string{fmt.Sprintf("%s,abc=def", testTable)}) + assert.Zero(t, qdb.BufLen(sender)) + + err = sender.Table(testTable).Symbol("ghi", "jkl").AtNow(ctx) + assert.NoError(t, err) + + err = sender.Flush(ctx) + assert.NoError(t, err) + + expectLines(t, srv.BackCh, []string{fmt.Sprintf("%s,ghi=jkl", testTable)}) +} + +func TestCustomTransportAndTlsInit(t *testing.T) { + ctx := context.Background() + + s1, err := qdb.NewLineSender(ctx, qdb.WithHttp()) + assert.NoError(t, err) + + s2, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithTls()) + assert.NoError(t, err) + + s3, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithTlsInsecureSkipVerify()) + assert.NoError(t, err) + + transport := http.Transport{} + s4, err := qdb.NewLineSender( + ctx, + qdb.WithHttp(), + qdb.WithHttpTransport(&transport), + qdb.WithTls(), + ) + assert.NoError(t, err) + + // s1 and s2 have successfully instantiated a sender + // using the global transport and should be registered in the + // global transport client count + assert.Equal(t, int64(2), qdb.GlobalTransport.ClientCount()) + + // Closing the client with the custom transport should not impact + // the global transport client count + s4.Close(ctx) + + // Now close all remaining clients + s1.Close(ctx) + s2.Close(ctx) + s3.Close(ctx) + assert.Equal(t, int64(0), qdb.GlobalTransport.ClientCount()) +} + +func BenchmarkHttpLineSenderBatch1000(b *testing.B) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(b, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 1000; j++ { + sender. + Table(testTable). + Symbol("sym_col", "test_ilp1"). + Float64Column("double_col", float64(i)+0.42). + Int64Column("long_col", int64(i)). + StringColumn("str_col", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"). + BoolColumn("bool_col", true). + TimestampColumn("timestamp_col", time.UnixMicro(42)). + At(ctx, time.UnixMicro(int64(1000*i))) + } + sender.Flush(ctx) + sender.Close(ctx) + } +} + +func BenchmarkHttpLineSenderNoFlush(b *testing.B) { + ctx := context.Background() + + srv, err := newTestHttpServer(readAndDiscard) + assert.NoError(b, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + sender. + Table(testTable). + Symbol("sym_col", "test_ilp1"). + Float64Column("double_col", float64(i)+0.42). + Int64Column("long_col", int64(i)). + StringColumn("str_col", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"). + BoolColumn("bool_col", true). + TimestampColumn("timestamp_col", time.UnixMicro(42)). + At(ctx, time.UnixMicro(int64(1000*i))) + } + sender.Flush(ctx) + sender.Close(ctx) +} diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..27b6dd8 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,456 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "context" + "fmt" + "math/big" + "path/filepath" + "reflect" + "testing" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// Common integration tests for ILP/HTTP and ILP/TCP. + +const ( + eventualDataTimeout = 60 * time.Second +) + +type integrationTestSuite struct { + suite.Suite +} + +func TestIntegrationSuite(t *testing.T) { + suite.Run(t, new(integrationTestSuite)) +} + +type writerFn func(b qdb.LineSender) error + +type questdbContainer struct { + testcontainers.Container + proxyC testcontainers.Container + network testcontainers.Network + httpAddress string + ilpAddress string + proxyIlpTcpAddress string + proxyIlpHttpAddress string +} + +func (c *questdbContainer) Stop(ctx context.Context) error { + if c.proxyC != nil { + err := c.proxyC.Terminate(ctx) + if err != nil { + return err + } + } + err := c.Terminate(ctx) + if err != nil { + return err + } + err = c.network.Remove(ctx) + if err != nil { + return err + } + return nil +} + +type ilpAuthType int64 + +const ( + noAuth ilpAuthType = 0 + authEnabled ilpAuthType = 1 + httpBasicAuth ilpAuthType = 2 + httpBearerAuth ilpAuthType = 3 +) + +const ( + basicAuthUser = "joe" + basicAuthPass = "joespassword" + bearerToken = "testToken1" +) + +func setupQuestDB(ctx context.Context, auth ilpAuthType) (*questdbContainer, error) { + return setupQuestDB0(ctx, auth, false) +} + +func setupQuestDBWithProxy(ctx context.Context, auth ilpAuthType) (*questdbContainer, error) { + return setupQuestDB0(ctx, auth, true) +} + +func setupQuestDB0(ctx context.Context, auth ilpAuthType, setupProxy bool) (*questdbContainer, error) { + // Make sure that ingested rows are committed almost immediately. + env := map[string]string{ + "QDB_CAIRO_MAX_UNCOMMITTED_ROWS": "1", + "QDB_LINE_TCP_MAINTENANCE_JOB_INTERVAL": "100", + "QDB_PG_ENABLED": "true", + "QDB_HTTP_MIN_ENABLED": "false", + "QDB_LINE_HTTP_ENABLED": "true", + } + + switch auth { + case authEnabled: + env["QDB_LINE_TCP_AUTH_DB_PATH"] = "/auth/questdb.auth.txt" + case httpBasicAuth: + env["QDB_PG_USER"] = basicAuthUser + env["QDB_PG_PASSWORD"] = basicAuthPass + case httpBearerAuth: + return nil, fmt.Errorf("idk how to set up bearer auth") + } + + path, err := filepath.Abs("./test") + if err != nil { + return nil, err + } + req := testcontainers.ContainerRequest{ + Image: "questdb/questdb:7.3.10", + ExposedPorts: []string{"9000/tcp", "9009/tcp"}, + WaitingFor: wait.ForHTTP("/").WithPort("9000"), + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"questdb"}}, + Env: env, + Mounts: testcontainers.Mounts(testcontainers.ContainerMount{ + Source: testcontainers.GenericBindMountSource{ + HostPath: path, + }, + Target: testcontainers.ContainerMountTarget("/root/.questdb/auth"), + }), + } + + newNetwork, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ + NetworkRequest: testcontainers.NetworkRequest{ + Name: networkName, + CheckDuplicate: true, + }, + }) + if err != nil { + return nil, err + } + + qdbC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + newNetwork.Remove(ctx) + return nil, err + } + + ip, err := qdbC.Host(ctx) + if err != nil { + newNetwork.Remove(ctx) + return nil, err + } + + mappedPort, err := qdbC.MappedPort(ctx, "9000") + if err != nil { + newNetwork.Remove(ctx) + return nil, err + } + httpAddress := fmt.Sprintf("%s:%s", ip, mappedPort.Port()) + + mappedPort, err = qdbC.MappedPort(ctx, "9009") + if err != nil { + newNetwork.Remove(ctx) + return nil, err + } + ilpAddress := fmt.Sprintf("%s:%s", ip, mappedPort.Port()) + + var ( + haProxyC testcontainers.Container + proxyIlpTcpAddress string + proxyIlpHttpAddress string + ) + if setupProxy || auth == httpBasicAuth || auth == httpBearerAuth { + req = testcontainers.ContainerRequest{ + Image: "haproxy:2.6.0", + ExposedPorts: []string{"8443/tcp", "8444/tcp", "8445/tcp", "8888/tcp"}, + WaitingFor: wait.ForHTTP("/").WithPort("8888"), + Networks: []string{networkName}, + Mounts: testcontainers.Mounts(testcontainers.ContainerMount{ + Source: testcontainers.GenericBindMountSource{ + HostPath: path, + }, + Target: testcontainers.ContainerMountTarget("/usr/local/etc/haproxy"), + }), + } + haProxyC, err = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + qdbC.Terminate(ctx) + newNetwork.Remove(ctx) + return nil, err + } + + ip, err := haProxyC.Host(ctx) + if err != nil { + qdbC.Terminate(ctx) + newNetwork.Remove(ctx) + return nil, err + } + + mappedPort, err := haProxyC.MappedPort(ctx, "8443") + if err != nil { + qdbC.Terminate(ctx) + newNetwork.Remove(ctx) + return nil, err + } + proxyIlpTcpAddress = fmt.Sprintf("%s:%s", ip, mappedPort.Port()) + + mappedPort, err = haProxyC.MappedPort(ctx, "8445") + if err != nil { + qdbC.Terminate(ctx) + newNetwork.Remove(ctx) + return nil, err + } + proxyIlpHttpAddress = fmt.Sprintf("%s:%s", ip, mappedPort.Port()) + } + + return &questdbContainer{ + Container: qdbC, + proxyC: haProxyC, + network: newNetwork, + httpAddress: httpAddress, + ilpAddress: ilpAddress, + proxyIlpTcpAddress: proxyIlpTcpAddress, + proxyIlpHttpAddress: proxyIlpHttpAddress, + }, nil +} + +func (suite *integrationTestSuite) TestE2EValidWrites() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + testCases := []struct { + name string + tableName string + writerFn writerFn + expected tableData + }{ + { + "all column types", + testTable, + func(s qdb.LineSender) error { + val, _ := big.NewInt(0).SetString("123a4", 16) + err := s. + Table(testTable). + Symbol("sym_col", "test_ilp1"). + Float64Column("double_col", 12.2). + Int64Column("long_col", 12). + Long256Column("long256_col", val). + StringColumn("str_col", "foobar"). + BoolColumn("bool_col", true). + TimestampColumn("timestamp_col", time.UnixMicro(42)). + At(ctx, time.UnixMicro(1)) + if err != nil { + return err + } + + val, _ = big.NewInt(0).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + return s. + Table(testTable). + Symbol("sym_col", "test_ilp2"). + Float64Column("double_col", 11.2). + Int64Column("long_col", 11). + Long256Column("long256_col", val). + StringColumn("str_col", "barbaz"). + BoolColumn("bool_col", false). + TimestampColumn("timestamp_col", time.UnixMicro(43)). + At(ctx, time.UnixMicro(2)) + }, + tableData{ + Columns: []column{ + {"sym_col", "SYMBOL"}, + {"double_col", "DOUBLE"}, + {"long_col", "LONG"}, + {"long256_col", "LONG256"}, + {"str_col", "STRING"}, + {"bool_col", "BOOLEAN"}, + {"timestamp_col", "TIMESTAMP"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"test_ilp1", float64(12.2), float64(12), "0x0123a4", "foobar", true, "1970-01-01T00:00:00.000042Z", "1970-01-01T00:00:00.000001Z"}, + {"test_ilp2", float64(11.2), float64(11), "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "barbaz", false, "1970-01-01T00:00:00.000043Z", "1970-01-01T00:00:00.000002Z"}, + }, + Count: 2, + }, + }, + { + "escaped chars", + "my-awesome_test 1=2.csv", + func(s qdb.LineSender) error { + return s. + Table("my-awesome_test 1=2.csv"). + Symbol("sym_name 1=2", "value 1,2=3\n4\r5\"6\\7"). + StringColumn("str_name 1=2", "value 1,2=3\n4\r5\"6\\7"). + At(ctx, time.UnixMicro(1)) + }, + tableData{ + Columns: []column{ + {"sym_name 1=2", "SYMBOL"}, + {"str_name 1=2", "STRING"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"value 1,2=3\n4\r5\"6\\7", "value 1,2=3\n4\r5\"6\\7", "1970-01-01T00:00:00.000001Z"}, + }, + Count: 1, + }, + }, + { + "single symbol", + testTable, + func(s qdb.LineSender) error { + return s. + Table(testTable). + Symbol("foo", "bar"). + At(ctx, time.UnixMicro(42)) + }, + tableData{ + Columns: []column{ + {"foo", "SYMBOL"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"bar", "1970-01-01T00:00:00.000042Z"}, + }, + Count: 1, + }, + }, + { + "single column", + testTable, + func(s qdb.LineSender) error { + return s. + Table(testTable). + Int64Column("foobar", 1_000_042). + At(ctx, time.UnixMicro(42)) + }, + tableData{ + Columns: []column{ + {"foobar", "LONG"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {float64(1_000_042), "1970-01-01T00:00:00.000042Z"}, + }, + Count: 1, + }, + }, + { + "single column long256", + testTable, + func(s qdb.LineSender) error { + val, _ := big.NewInt(0).SetString("7fffffffffffffff", 16) + return s. + Table(testTable). + Long256Column("foobar", val). + At(ctx, time.UnixMicro(42)) + }, + tableData{ + Columns: []column{ + {"foobar", "LONG256"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"0x7fffffffffffffff", "1970-01-01T00:00:00.000042Z"}, + }, + Count: 1, + }, + }, + { + "double value with exponent", + testTable, + func(s qdb.LineSender) error { + return s. + Table(testTable). + Float64Column("foobar", 4.2e-100). + At(ctx, time.UnixMicro(1)) + }, + tableData{ + Columns: []column{ + {"foobar", "DOUBLE"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {4.2e-100, "1970-01-01T00:00:00.000001Z"}, + }, + Count: 1, + }, + }, + } + + for _, tc := range testCases { + for _, protocol := range []string{"tcp", "http"} { + suite.T().Run(fmt.Sprintf("%s: %s", tc.name, protocol), func(t *testing.T) { + var ( + sender qdb.LineSender + err error + ) + + questdbC, err := setupQuestDB(ctx, noAuth) + assert.NoError(t, err) + + switch protocol { + case "tcp": + sender, err = qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(questdbC.ilpAddress)) + assert.NoError(t, err) + case "http": + sender, err = qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(questdbC.httpAddress)) + assert.NoError(t, err) + default: + panic(protocol) + } + + err = tc.writerFn(sender) + assert.NoError(t, err) + + err = sender.Flush(ctx) + assert.NoError(t, err) + + assert.Eventually(t, func() bool { + data := queryTableData(t, tc.tableName, questdbC.httpAddress) + return reflect.DeepEqual(tc.expected, data) + }, eventualDataTimeout, 100*time.Millisecond) + + sender.Close(ctx) + questdbC.Stop(ctx) + }) + } + } +} diff --git a/sender_interop_test.go b/interop_test.go similarity index 63% rename from sender_interop_test.go rename to interop_test.go index 73814c6..089c777 100644 --- a/sender_interop_test.go +++ b/interop_test.go @@ -27,12 +27,12 @@ package questdb_test import ( "context" "encoding/json" - "io/ioutil" + "io" "os" "strings" "testing" - qdb "github.com/questdb/go-questdb-client/v2" + qdb "github.com/questdb/go-questdb-client/v3" "github.com/stretchr/testify/assert" ) @@ -64,7 +64,7 @@ type testResult struct { Line string `json:"line"` } -func TestClientInterop(t *testing.T) { +func TestTcpClientInterop(t *testing.T) { ctx := context.Background() testCases, err := readTestCases() @@ -72,10 +72,10 @@ func TestClientInterop(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - srv, err := newTestServer(sendToBackChannel) + srv, err := newTestTcpServer(sendToBackChannel) assert.NoError(t, err) - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(srv.Addr())) assert.NoError(t, err) sender.Table(tc.Table) @@ -105,27 +105,81 @@ func TestClientInterop(t *testing.T) { err = sender.Flush(ctx) assert.NoError(t, err) - expectLines(t, srv.backCh, strings.Split(tc.Result.Line, "\n")) + expectLines(t, srv.BackCh, strings.Split(tc.Result.Line, "\n")) case "ERROR": assert.Error(t, err) default: assert.Fail(t, "unexpected test status: "+tc.Result.Status) } - sender.Close() - srv.close() + sender.Close(ctx) + srv.Close() + }) + } +} + +func TestHttpClientInterop(t *testing.T) { + ctx := context.Background() + + testCases, err := readTestCases() + assert.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + srv, err := newTestHttpServer(sendToBackChannel) + assert.NoError(t, err) + + sender, err := qdb.NewLineSender(ctx, qdb.WithHttp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + + sender.Table(tc.Table) + for _, s := range tc.Symbols { + sender.Symbol(s.Name, s.Value) + } + for _, s := range tc.Columns { + switch s.Type { + case "LONG": + sender.Int64Column(s.Name, int64(s.Value.(float64))) + case "DOUBLE": + sender.Float64Column(s.Name, s.Value.(float64)) + case "STRING": + sender.StringColumn(s.Name, s.Value.(string)) + case "BOOLEAN": + sender.BoolColumn(s.Name, s.Value.(bool)) + default: + assert.Fail(t, "unexpected column type: "+s.Type) + } + } + + err = sender.AtNow(ctx) + + switch tc.Result.Status { + case "SUCCESS": + assert.NoError(t, err) + err = sender.Flush(ctx) + assert.NoError(t, err) + + expectLines(t, srv.BackCh, strings.Split(tc.Result.Line, "\n")) + case "ERROR": + assert.Error(t, err) + default: + assert.Fail(t, "unexpected test status: "+tc.Result.Status) + } + + sender.Close(ctx) + srv.Close() }) } } func readTestCases() (testCases, error) { - file, err := os.Open("./test/interop/ilp-client-interop-test.json") + file, err := os.Open("./test/interop/questdb-client-test/ilp-client-interop-test.json") if err != nil { return nil, err } defer file.Close() - content, err := ioutil.ReadAll(file) + content, err := io.ReadAll(file) if err != nil { return nil, err } diff --git a/sender.go b/sender.go index c8b358f..2a73431 100644 --- a/sender.go +++ b/sender.go @@ -25,32 +25,146 @@ package questdb import ( - "bufio" - "bytes" "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "encoding/base64" "errors" "fmt" - "math" "math/big" - "net" - "strconv" + "net/http" "time" ) -// ErrInvalidMsg indicates a failed attempt to construct an ILP -// message, e.g. duplicate calls to Table method or illegal -// chars found in table or column name. -var ErrInvalidMsg = errors.New("invalid message") +// LineSender allows you to insert rows into QuestDB by sending ILP +// messages over HTTP or TCP protocol. +// +// Each sender corresponds to a single client-server connection. +// A sender should not be called concurrently by multiple goroutines. +// +// HTTP senders also reuse connections from a global pool by default. +type LineSender interface { + // Table sets the table name (metric) for a new ILP message. Should be + // called before any Symbol or Column method. + // + // Table name cannot contain any of the following characters: + // '\n', '\r', '?', ',', ”', '"', '\', '/', ':', ')', '(', '+', '*', + // '%', '~', starting '.', trailing '.', or a non-printable char. + Table(name string) LineSender + + // Symbol adds a symbol column value to the ILP message. Should be called + // before any Column method. + // + // Symbol name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + Symbol(name, val string) LineSender + + // Int64Column adds a 64-bit integer (long) column value to the ILP + // message. + // + // Column name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + Int64Column(name string, val int64) LineSender + + // Long256Column adds a 256-bit unsigned integer (long256) column + // value to the ILP message. + // + // Only non-negative numbers that fit into 256-bit unsigned integer are + // supported and any other input value would lead to an error. + // + // Column name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + Long256Column(name string, val *big.Int) LineSender + + // TimestampColumn adds a timestamp column value to the ILP + // message. + // + // Column name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + TimestampColumn(name string, ts time.Time) LineSender + + // Float64Column adds a 64-bit float (double) column value to the ILP + // message. + // + // Column name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + Float64Column(name string, val float64) LineSender + + // StringColumn adds a string column value to the ILP message. + // + // Column name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + StringColumn(name, val string) LineSender + + // BoolColumn adds a boolean column value to the ILP message. + // + // Column name cannot contain any of the following characters: + // '\n', '\r', '?', '.', ',', ”', '"', '\', '/', ':', ')', '(', '+', + // '-', '*' '%%', '~', or a non-printable char. + BoolColumn(name string, val bool) LineSender + + // At sets the timestamp in Epoch nanoseconds and finalizes + // the ILP message. + // + // If the underlying buffer reaches configured capacity or the + // number of buffered messages exceeds the auto-flush trigger, this + // method also sends the accumulated messages. + // + // If ts.IsZero(), no timestamp is sent to the server. + At(ctx context.Context, ts time.Time) error + + // AtNow omits the timestamp and finalizes the ILP message. + // The server will insert each message using the system clock + // as the row timestamp. + // + // If the underlying buffer reaches configured capacity or the + // number of buffered messages exceeds the auto-flush trigger, this + // method also sends the accumulated messages. + AtNow(ctx context.Context) error + + // Flush sends the accumulated messages via the underlying + // connection. Should be called periodically to make sure that + // all messages are sent to the server. + // + // For optimal performance, this method should not be called after + // each ILP message. Instead, the messages should be written in + // batches followed by a Flush call. The optimal batch size may + // vary from one thousand to few thousand messages depending on + // the message size. + Flush(ctx context.Context) error + + // Close closes the underlying HTTP client. + // + // If auto-flush is enabled, the client will flush any remaining buffered + // messages before closing itself. + Close(ctx context.Context) error +} const ( - defaultBufferCapacity = 128 * 1024 + defaultHttpAddress = "127.0.0.1:9000" + defaultTcpAddress = "127.0.0.1:9009" + + defaultInitBufferSize = 128 * 1024 // 128KB + defaultMaxBufferSize = 100 * 1024 * 1024 // 100MB defaultFileNameLimit = 127 + + defaultAutoFlushRows = 75000 + defaultAutoFlushInterval = time.Second + + defaultMinThroughput = 100 * 1024 // 100KB/s + defaultRetryTimeout = 10 * time.Second + defaultRequestTimeout = 10 * time.Second +) + +type senderType int64 + +const ( + noSenderType senderType = 0 + httpSenderType senderType = 1 + tcpSenderType senderType = 2 ) type tlsMode int64 @@ -61,799 +175,411 @@ const ( tlsInsecureSkipVerify tlsMode = 2 ) -// LineSender allows you to insert rows into QuestDB by sending ILP -// messages. -// -// Each sender corresponds to a single TCP connection. A sender -// should not be called concurrently by multiple goroutines. -type LineSender struct { +type lineSenderConfig struct { + senderType senderType address string - tlsMode tlsMode - keyId string // Erased once auth is done. - key string // Erased once auth is done. - bufCap int + initBufSize int + maxBufSize int fileNameLimit int - conn net.Conn - buf *buffer - lastMsgPos int - lastErr error - hasTable bool - hasTags bool - hasFields bool + httpTransport *http.Transport + + // Retry/timeout-related fields + retryTimeout time.Duration + minThroughput int + requestTimeout time.Duration + + // Authentication-related fields + tlsMode tlsMode + tcpKeyId string + tcpKey string + httpUser string + httpPass string + httpToken string + + // Auto-flush fields + autoFlushRows int + autoFlushInterval time.Duration } -// LineSenderOption defines line sender option. -type LineSenderOption func(*LineSender) +// LineSenderOption defines line sender config option. +type LineSenderOption func(*lineSenderConfig) -// WithAddress sets address to connect to. Should be in the -// "host:port" format. Defaults to "127.0.0.1:9009". -func WithAddress(address string) LineSenderOption { - return func(s *LineSender) { - s.address = address +// WithHttp enables ingestion over HTTP protocol. +func WithHttp() LineSenderOption { + return func(s *lineSenderConfig) { + s.senderType = httpSenderType } } -// WithAuth sets token (private key) used for ILP authentication. -func WithAuth(tokenId, token string) LineSenderOption { - return func(s *LineSender) { - s.keyId = tokenId - s.key = token +// WithTcp enables ingestion over TCP protocol. +func WithTcp() LineSenderOption { + return func(s *lineSenderConfig) { + s.senderType = tcpSenderType } } // WithTls enables TLS connection encryption. func WithTls() LineSenderOption { - return func(s *LineSender) { + return func(s *lineSenderConfig) { s.tlsMode = tlsEnabled } } -// WithTlsInsecureSkipVerify enables TLS connection encryption, -// but skips server certificate verification. Useful in test -// environments with self-signed certificates. Do not use in -// production environments. -func WithTlsInsecureSkipVerify() LineSenderOption { - return func(s *LineSender) { - s.tlsMode = tlsInsecureSkipVerify +// WithAuth sets token (private key) used for ILP authentication. +// +// Only available for the TCP sender. +func WithAuth(tokenId, token string) LineSenderOption { + return func(s *lineSenderConfig) { + s.tcpKeyId = tokenId + s.tcpKey = token } } -// WithBufferCapacity sets desired buffer capacity in bytes to -// be used when sending ILP messages. Defaults to 128KB. +// WithBasicAuth sets a Basic authentication header for +// ILP requests over HTTP. // -// This setting is a soft limit, i.e. the underlying buffer may -// grow larger than the provided value, but will shrink on a -// At, AtNow, or Flush call. -func WithBufferCapacity(capacity int) LineSenderOption { - return func(s *LineSender) { - if capacity > 0 { - s.bufCap = capacity - } +// Only available for the HTTP sender. +func WithBasicAuth(user, pass string) LineSenderOption { + return func(s *lineSenderConfig) { + s.httpUser = user + s.httpPass = pass } } -// WithFileNameLimit sets maximum file name length in chars -// allowed by the server. Affects maximum table and column name -// lengths accepted by the sender. Should be set to the same value -// as on the server. Defaults to 127. -func WithFileNameLimit(limit int) LineSenderOption { - return func(s *LineSender) { - if limit > 0 { - s.fileNameLimit = limit - } +// WithBearerToken sets a Bearer token Authentication header for +// ILP requests. +// +// Only available for the HTTP sender. +func WithBearerToken(token string) LineSenderOption { + return func(s *lineSenderConfig) { + s.httpToken = token } } -// NewLineSender creates new InfluxDB Line Protocol (ILP) sender. Each -// sender corresponds to a single TCP connection. Sender should -// not be called concurrently by multiple goroutines. -func NewLineSender(ctx context.Context, opts ...LineSenderOption) (*LineSender, error) { - var ( - d net.Dialer - key *ecdsa.PrivateKey - conn net.Conn - err error - ) - - s := &LineSender{ - address: "127.0.0.1:9009", - bufCap: defaultBufferCapacity, - fileNameLimit: defaultFileNameLimit, - tlsMode: tlsDisabled, - } - for _, opt := range opts { - opt(s) +// WithRequestTimeout is used in combination with min_throughput +// to set the timeout of an ILP request. Defaults to 10 seconds. +// +// timeout = (request.len() / min_throughput) + request_timeout +// +// Only available for the HTTP sender. +func WithRequestTimeout(timeout time.Duration) LineSenderOption { + return func(s *lineSenderConfig) { + s.requestTimeout = timeout } +} - if s.keyId != "" && s.key != "" { - keyRaw, err := base64.RawURLEncoding.DecodeString(s.key) - if err != nil { - return nil, fmt.Errorf("failed to decode auth key: %v", err) - } - key = new(ecdsa.PrivateKey) - key.PublicKey.Curve = elliptic.P256() - key.PublicKey.X, key.PublicKey.Y = key.PublicKey.Curve.ScalarBaseMult(keyRaw) - key.D = new(big.Int).SetBytes(keyRaw) - } - - if s.tlsMode == tlsDisabled { - conn, err = d.DialContext(ctx, "tcp", s.address) - } else { - config := &tls.Config{} - if s.tlsMode == tlsInsecureSkipVerify { - config.InsecureSkipVerify = true - } - conn, err = tls.DialWithDialer(&d, "tcp", s.address, config) - } - if err != nil { - return nil, fmt.Errorf("failed to connect to server: %v", err) +// WithMinThroughput is used in combination with request_timeout +// to set the timeout of an ILP request. Defaults to 100KiB/s. +// +// timeout = (request.len() / min_throughput) + request_timeout +// +// Only available for the HTTP sender. +func WithMinThroughput(bytesPerSecond int) LineSenderOption { + return func(s *lineSenderConfig) { + s.minThroughput = bytesPerSecond } +} - if key != nil { - if deadline, ok := ctx.Deadline(); ok { - conn.SetDeadline(deadline) - } - - _, err = conn.Write([]byte(s.keyId + "\n")) - if err != nil { - conn.Close() - return nil, fmt.Errorf("failed to write key id: %v", err) - } - - reader := bufio.NewReader(conn) - raw, err := reader.ReadBytes('\n') - if len(raw) < 2 { - conn.Close() - return nil, fmt.Errorf("empty challenge response from server: %v", err) - } - // Remove the `\n` in the last position. - raw = raw[:len(raw)-1] - if err != nil { - conn.Close() - return nil, fmt.Errorf("failed to read challenge response from server: %v", err) - } - - // Hash the challenge with sha256. - hash := crypto.SHA256.New() - hash.Write(raw) - hashed := hash.Sum(nil) - - stdSig, err := ecdsa.SignASN1(rand.Reader, key, hashed) - if err != nil { - conn.Close() - return nil, fmt.Errorf("failed to sign challenge using auth key: %v", err) - } - _, err = conn.Write([]byte(base64.StdEncoding.EncodeToString(stdSig) + "\n")) - if err != nil { - conn.Close() - return nil, fmt.Errorf("failed to write signed challenge: %v", err) - } - - // Reset the deadline. - conn.SetDeadline(time.Time{}) - // Erase the key values since we don't need them anymore. - s.key = "" - s.keyId = "" +// WithRetryTimeout is the cumulative maximum duration spend in +// retries. Defaults to 10 seconds. Retries work great when +// used in combination with server-side data deduplication. +// +// Only network-related errors and certain 5xx response +// codes are retryable. +// +// Only available for the HTTP sender. +func WithRetryTimeout(t time.Duration) LineSenderOption { + return func(s *lineSenderConfig) { + s.retryTimeout = t } - - s.conn = conn - s.buf = newBuffer(s.bufCap) - return s, nil } -// Close closes the underlying TCP connection. Does not flush -// in-flight messages, so make sure to call Flush first. -func (s *LineSender) Close() error { - return s.conn.Close() +// WithInitBufferSize sets the desired initial buffer capacity +// in bytes to be used when sending ILP messages. Defaults to 128KB. +// +// This setting is a soft limit, i.e. the underlying buffer may +// grow larger than the provided value. +func WithInitBufferSize(sizeInBytes int) LineSenderOption { + return func(s *lineSenderConfig) { + s.initBufSize = sizeInBytes + } } -// Table sets the table name (metric) for a new ILP message. Should be -// called before any Symbol or Column method. +// WithMaxBufferSize sets the maximum buffer capacity +// in bytes to be used when sending ILP messages. The sender will +// return an error if the limit is reached. Defaults to 100MB. // -// Table name cannot contain any of the following characters: -// '\n', '\r', '?', ',', ”', '"', '\', '/', ':', ')', '(', '+', '*', -// '%', '~', starting '.', trailing '.', or a non-printable char. -func (s *LineSender) Table(name string) *LineSender { - if s.lastErr != nil { - return s - } - if s.hasTable { - s.lastErr = fmt.Errorf("table name already provided: %w", ErrInvalidMsg) - return s - } - s.lastErr = s.writeTableName(name) - if s.lastErr != nil { - return s - } - s.hasTable = true - return s +// Only available for the HTTP sender. +func WithMaxBufferSize(sizeInBytes int) LineSenderOption { + return func(s *lineSenderConfig) { + s.maxBufSize = sizeInBytes + } } -// Symbol adds a symbol column value to the ILP message. Should be called -// before any Column method. -// -// Symbol name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) Symbol(name, val string) *LineSender { - if s.lastErr != nil { - return s - } - if !s.hasTable { - s.lastErr = fmt.Errorf("table name was not provided: %w", ErrInvalidMsg) - return s - } - if s.hasFields { - s.lastErr = fmt.Errorf("symbols have to be written before any other column: %w", ErrInvalidMsg) - return s - } - s.buf.WriteByte(',') - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - s.lastErr = s.writeStrValue(val, false) - if s.lastErr != nil { - return s - } - s.hasTags = true - return s +// WithFileNameLimit sets maximum file name length in chars +// allowed by the server. Affects maximum table and column name +// lengths accepted by the sender. Should be set to the same value +// as on the server. Defaults to 127. +func WithFileNameLimit(limit int) LineSenderOption { + return func(s *lineSenderConfig) { + s.fileNameLimit = limit + } } -// Int64Column adds a 64-bit integer (long) column value to the ILP -// message. -// -// Column name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) Int64Column(name string, val int64) *LineSender { - if !s.prepareForField(name) { - return s - } - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - s.buf.WriteInt(val) - s.buf.WriteByte('i') - s.hasFields = true - return s +// WithAddress sets address to connect to. Should be in the +// "host:port" format. Defaults to "127.0.0.1:9000" in case +// of HTTP and "127.0.0.1:9009" in case of TCP. +func WithAddress(addr string) LineSenderOption { + return func(s *lineSenderConfig) { + s.address = addr + } } -// Long256Column adds a 256-bit unsigned integer (long256) column -// value to the ILP message. -// -// Only non-negative numbers that fit into 256-bit unsigned integer are -// supported and any other input value would lead to an error. -// -// Column name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) Long256Column(name string, val *big.Int) *LineSender { - if val.Sign() < 0 { - if s.lastErr != nil { - return s - } - s.lastErr = fmt.Errorf("long256 cannot be negative: %s", val.String()) - return s +// WithTlsInsecureSkipVerify enables TLS connection encryption, +// but skips server certificate verification. Useful in test +// environments with self-signed certificates. Do not use in +// production environments. +func WithTlsInsecureSkipVerify() LineSenderOption { + return func(s *lineSenderConfig) { + s.tlsMode = tlsInsecureSkipVerify } - if val.BitLen() > 256 { - if s.lastErr != nil { - return s - } - s.lastErr = fmt.Errorf("long256 cannot be larger than 256-bit: %v", val.BitLen()) - return s - } - if !s.prepareForField(name) { - return s - } - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - s.buf.WriteByte('0') - s.buf.WriteByte('x') - s.buf.WriteBigInt(val) - s.buf.WriteByte('i') - if s.lastErr != nil { - return s - } - s.hasFields = true - return s } -// TimestampColumn adds a timestamp column value to the ILP -// message. +// WithHttpTransport sets the client's http transport to the +// passed pointer instead of the global transport. This can be +// used for customizing the http transport used by the LineSender. +// WithTlsInsecureSkipVerify is ignored when this option is in use. // -// Column name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) TimestampColumn(name string, ts time.Time) *LineSender { - if !s.prepareForField(name) { - return s - } - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - s.buf.WriteInt(ts.UnixMicro()) - s.buf.WriteByte('t') - s.hasFields = true - return s +// Only available for the HTTP sender. +func WithHttpTransport(t *http.Transport) LineSenderOption { + return func(s *lineSenderConfig) { + s.httpTransport = t + } } -// Float64Column adds a 64-bit float (double) column value to the ILP -// message. +// WithAutoFlushDisabled turns off auto-flushing behavior. +// To send ILP messages, the user must call Flush(). // -// Column name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) Float64Column(name string, val float64) *LineSender { - if !s.prepareForField(name) { - return s - } - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - s.buf.WriteFloat(val) - s.hasFields = true - return s +// Only available for the HTTP sender. +func WithAutoFlushDisabled() LineSenderOption { + return func(s *lineSenderConfig) { + s.autoFlushRows = 0 + s.autoFlushInterval = 0 + } } -// StringColumn adds a string column value to the ILP message. +// WithAutoFlushRows sets the number of buffered rows that +// must be breached in order to trigger an auto-flush. +// Defaults to 75000. // -// Column name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) StringColumn(name, val string) *LineSender { - if !s.prepareForField(name) { - return s - } - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - s.buf.WriteByte('"') - s.lastErr = s.writeStrValue(val, true) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('"') - s.hasFields = true - return s +// Only available for the HTTP sender. +func WithAutoFlushRows(rows int) LineSenderOption { + return func(s *lineSenderConfig) { + s.autoFlushRows = rows + } } -// BoolColumn adds a boolean column value to the ILP message. +// WithAutoFlushInterval the interval at which the Sender +// automatically flushes its buffer. Defaults to 1 second. // -// Column name cannot contain any of the following characters: -// '\n', '\r', '?', '.', ',', ”', '"', '\', '/', ':', ')', '(', '+', -// '-', '*' '%%', '~', or a non-printable char. -func (s *LineSender) BoolColumn(name string, val bool) *LineSender { - if !s.prepareForField(name) { - return s - } - s.lastErr = s.writeColumnName(name) - if s.lastErr != nil { - return s - } - s.buf.WriteByte('=') - if val { - s.buf.WriteByte('t') - } else { - s.buf.WriteByte('f') - } - s.hasFields = true - return s +// Only available for the HTTP sender. +func WithAutoFlushInterval(interval time.Duration) LineSenderOption { + return func(s *lineSenderConfig) { + s.autoFlushInterval = interval + } } -func (s *LineSender) writeTableName(str string) error { - if str == "" { - return fmt.Errorf("table name cannot be empty: %w", ErrInvalidMsg) - } - // We use string length in bytes as an approximation. That's to - // avoid calculating the number of runes. - if len(str) > s.fileNameLimit { - return fmt.Errorf("table name length exceeds the limit: %w", ErrInvalidMsg) - } - // Since we're interested in ASCII chars, it's fine to iterate - // through bytes instead of runes. - for i := 0; i < len(str); i++ { - b := str[i] - switch b { - case ' ': - s.buf.WriteByte('\\') - case '=': - s.buf.WriteByte('\\') - case '.': - if i == 0 || i == len(str)-1 { - return fmt.Errorf("table name contains '.' char at the start or end: %s: %w", str, ErrInvalidMsg) - } - default: - if illegalTableNameChar(b) { - return fmt.Errorf("table name contains an illegal char: "+ - "'\\n', '\\r', '?', ',', ''', '\"', '\\', '/', ':', ')', '(', '+', '*' '%%', '~', or a non-printable char: %s: %w", - str, ErrInvalidMsg) - } - } - s.buf.WriteByte(b) +// LineSenderFromConf creates a LineSender using the QuestDB config string format. +// +// Example config string: "http::addr=localhost;username=joe;password=123;auto_flush_rows=1000;" +// +// QuestDB ILP clients use a common key-value configuration string format across all +// implementations. We opted for this config over a URL because it reduces the amount +// of character escaping required for paths and base64-encoded param values. +// +// The config string format is as follows: +// +// schema::key1=value1;key2=value2;key3=value3; +// +// Schemas supported are "http", "https", "tcp", "tcps" +// +// Options: +// http(s) and tcp(s): +// ------------------- +// addr: hostname/port of QuestDB endpoint +// init_buf_size: initial growable ILP buffer size in bytes (defaults to 128KiB) +// tls_verify: determines if TLS certificates should be validated (defaults to "on", can be set to "unsafe_off") +// +// http(s)-only +// ------------ +// username: for basic authentication +// password: for basic authentication +// token: bearer token auth (used instead of basic authentication) +// auto_flush: determines if auto-flushing is enabled (values "on" or "off", defaults to "on") +// auto_flush_rows: auto-flushing is triggered above this row count (defaults to 75000). If set, explicitly implies auto_flush=on +// request_min_throughput: bytes per second, used to calculate each request's timeout (defaults to 100KiB/s) +// request_timeout: minimum request timeout in milliseconds (defaults to 10 seconds) +// retry_timeout: cumulative maximum millisecond duration spent in retries (defaults to 10 seconds) +// max_buf_size: buffer growth limit in bytes. Client errors if breached (default is 100MiB) +// +// tcp(s)-only +// ----------- +// username: KID (key ID) for ECDSA authentication +// token: Secret K (D) for ECDSA authentication +func LineSenderFromConf(ctx context.Context, conf string) (LineSender, error) { + c, err := confFromStr(conf) + if err != nil { + return nil, err } - return nil + return newLineSender(ctx, c) } -func illegalTableNameChar(ch byte) bool { - switch ch { - case '\n': - return true - case '\r': - return true - case '?': - return true - case ',': - return true - case '\'': - return true - case '"': - return true - case '\\': - return true - case '/': - return true - case ':': - return true - case ')': - return true - case '(': - return true - case '+': - return true - case '*': - return true - case '%': - return true - case '~': - return true - case '\u0000': - return true - case '\u0001': - return true - case '\u0002': - return true - case '\u0003': - return true - case '\u0004': - return true - case '\u0005': - return true - case '\u0006': - return true - case '\u0007': - return true - case '\u0008': - return true - case '\u0009': - return true - case '\u000b': - return true - case '\u000c': - return true - case '\u000e': - return true - case '\u000f': - return true - case '\u007f': - return true - } - return false +// NewLineSender creates new InfluxDB Line Protocol (ILP) sender. Each +// sender corresponds to a single client connection. LineSender should +// not be called concurrently by multiple goroutines. +func NewLineSender(ctx context.Context, opts ...LineSenderOption) (LineSender, error) { + conf := &lineSenderConfig{} + for _, opt := range opts { + opt(conf) + } + return newLineSender(ctx, conf) } -func (s *LineSender) writeColumnName(str string) error { - if str == "" { - return fmt.Errorf("column name cannot be empty: %w", ErrInvalidMsg) - } - // We use string length in bytes as an approximation. That's to - // avoid calculating the number of runes. - if len(str) > s.fileNameLimit { - return fmt.Errorf("column name length exceeds the limit: %w", ErrInvalidMsg) - } - // Since we're interested in ASCII chars, it's fine to iterate - // through bytes instead of runes. - for i := 0; i < len(str); i++ { - b := str[i] - switch b { - case ' ': - s.buf.WriteByte('\\') - case '=': - s.buf.WriteByte('\\') - default: - if illegalColumnNameChar(b) { - return fmt.Errorf("column name contains an illegal char: "+ - "'\\n', '\\r', '?', '.', ',', ''', '\"', '\\', '/', ':', ')', '(', '+', '-', '*' '%%', '~', or a non-printable char: %s: %w", - str, ErrInvalidMsg) - } +func newLineSender(ctx context.Context, conf *lineSenderConfig) (LineSender, error) { + switch conf.senderType { + case tcpSenderType: + err := sanitizeTcpConf(conf) + if err != nil { + return nil, err } - s.buf.WriteByte(b) + return newTcpLineSender(ctx, conf) + case httpSenderType: + err := sanitizeHttpConf(conf) + if err != nil { + return nil, err + } + return newHttpLineSender(conf) } - return nil -} - -func illegalColumnNameChar(ch byte) bool { - switch ch { - case '\n': - return true - case '\r': - return true - case '?': - return true - case '.': - return true - case ',': - return true - case '\'': - return true - case '"': - return true - case '\\': - return true - case '/': - return true - case ':': - return true - case ')': - return true - case '(': - return true - case '+': - return true - case '-': - return true - case '*': - return true - case '%': - return true - case '~': - return true - case '\u0000': - return true - case '\u0001': - return true - case '\u0002': - return true - case '\u0003': - return true - case '\u0004': - return true - case '\u0005': - return true - case '\u0006': - return true - case '\u0007': - return true - case '\u0008': - return true - case '\u0009': - return true - case '\u000b': - return true - case '\u000c': - return true - case '\u000e': - return true - case '\u000f': - return true - case '\u007f': - return true - } - return false + return nil, errors.New("sender type is not specified: use WithHttp or WithTcp") } -func (s *LineSender) writeStrValue(str string, quoted bool) error { - // Since we're interested in ASCII chars, it's fine to iterate - // through bytes instead of runes. - for i := 0; i < len(str); i++ { - b := str[i] - switch b { - case ' ': - if !quoted { - s.buf.WriteByte('\\') - } - case ',': - if !quoted { - s.buf.WriteByte('\\') - } - case '=': - if !quoted { - s.buf.WriteByte('\\') - } - case '"': - if quoted { - s.buf.WriteByte('\\') - } - case '\n': - s.buf.WriteByte('\\') - case '\r': - s.buf.WriteByte('\\') - case '\\': - s.buf.WriteByte('\\') - } - s.buf.WriteByte(b) +func sanitizeTcpConf(conf *lineSenderConfig) error { + err := validateConf(conf) + if err != nil { + return err } - return nil -} -func (s *LineSender) prepareForField(name string) bool { - if s.lastErr != nil { - return false + // validate tcp-specific settings + if conf.requestTimeout != 0 { + return errors.New("requestTimeout setting is not available in the TCP client") } - if !s.hasTable { - s.lastErr = fmt.Errorf("table name was not provided: %w", ErrInvalidMsg) - return false + if conf.retryTimeout != 0 { + return errors.New("retryTimeout setting is not available in the TCP client") } - if !s.hasFields { - s.buf.WriteByte(' ') - } else { - s.buf.WriteByte(',') + if conf.minThroughput != 0 { + return errors.New("minThroughput setting is not available in the TCP client") } - return true -} - -// AtNow omits the timestamp and finalizes the ILP message. -// The server will insert each message using the system clock -// as the row timestamp. -// -// If the underlying buffer reaches configured capacity, this -// method also sends the accumulated messages. -func (s *LineSender) AtNow(ctx context.Context) error { - return s.at(ctx, time.Time{}, false) -} - -// At sets the timestamp in Epoch nanoseconds and finalizes -// the ILP message. -// -// If the underlying buffer reaches configured capacity, this -// method also sends the accumulated messages. -func (s *LineSender) At(ctx context.Context, ts time.Time) error { - return s.at(ctx, ts, true) -} - -func (s *LineSender) at(ctx context.Context, ts time.Time, sendTs bool) error { - err := s.lastErr - s.lastErr = nil - if err != nil { - s.discardPendingMsg() - return err + if conf.autoFlushRows != 0 { + return errors.New("autoFlushRows setting is not available in the TCP client") } - if !s.hasTable { - s.discardPendingMsg() - return fmt.Errorf("table name was not provided: %w", ErrInvalidMsg) + if conf.autoFlushInterval != 0 { + return errors.New("autoFlushInterval setting is not available in the TCP client") } - if !s.hasTags && !s.hasFields { - s.discardPendingMsg() - return fmt.Errorf("no symbols or columns were provided: %w", ErrInvalidMsg) + if conf.maxBufSize != 0 { + return errors.New("maxBufferSize setting is not available in the TCP client") } - - if sendTs { - s.buf.WriteByte(' ') - s.buf.WriteInt(ts.UnixNano()) + if conf.tcpKey == "" && conf.tcpKeyId != "" { + return errors.New("tcpKey is empty and tcpKeyId is not. both (or none) must be provided") + } + if conf.tcpKeyId == "" && conf.tcpKey != "" { + return errors.New("tcpKeyId is empty and tcpKey is not. both (or none) must be provided") } - s.buf.WriteByte('\n') - - s.lastMsgPos = s.buf.Len() - s.resetMsgFlags() - if s.buf.Len() > s.bufCap { - return s.Flush(ctx) + // Set defaults + if conf.address == "" { + conf.address = defaultTcpAddress + } + if conf.initBufSize == 0 { + conf.initBufSize = defaultInitBufferSize } + if conf.fileNameLimit == 0 { + conf.fileNameLimit = defaultFileNameLimit + } + return nil } -// Flush flushes the accumulated messages to the underlying TCP -// connection. Should be called periodically to make sure that -// all messages are sent to the server. -// -// For optimal performance, this method should not be called after -// each ILP message. Instead, the messages should be written in -// batches followed by a Flush call. Optimal batch size may vary -// from 100 to 1,000 messages depending on the message size and -// configured buffer capacity. -func (s *LineSender) Flush(ctx context.Context) error { - err := s.lastErr - s.lastErr = nil +func sanitizeHttpConf(conf *lineSenderConfig) error { + err := validateConf(conf) if err != nil { - s.discardPendingMsg() return err } - if s.hasTable { - s.discardPendingMsg() - return errors.New("pending ILP message must be finalized with At or AtNow before calling Flush") + + // validate http-specific settings + if (conf.httpUser != "" || conf.httpPass != "") && conf.httpToken != "" { + return errors.New("both basic and token authentication cannot be used") } - if err = ctx.Err(); err != nil { - return err + // Set defaults + if conf.address == "" { + conf.address = defaultHttpAddress } - if deadline, ok := ctx.Deadline(); ok { - s.conn.SetWriteDeadline(deadline) - } else { - s.conn.SetWriteDeadline(time.Time{}) + if conf.requestTimeout == 0 { + conf.requestTimeout = defaultRequestTimeout } - - n, err := s.buf.WriteTo(s.conn) - if err != nil { - s.lastMsgPos -= int(n) - return err + if conf.retryTimeout == 0 { + conf.retryTimeout = defaultRetryTimeout } - - // bytes.Buffer grows as 2*cap+n, so we use 3x as the threshold. - if s.buf.Cap() > 3*s.bufCap { - // Shrink the buffer back to desired capacity. - s.buf = newBuffer(s.bufCap) + if conf.minThroughput == 0 { + conf.minThroughput = defaultMinThroughput + } + if conf.autoFlushRows == 0 { + conf.autoFlushRows = defaultAutoFlushRows + } + if conf.autoFlushInterval == 0 { + conf.autoFlushInterval = defaultAutoFlushInterval + } + if conf.initBufSize == 0 { + conf.initBufSize = defaultInitBufferSize + } + if conf.maxBufSize == 0 { + conf.maxBufSize = defaultMaxBufferSize + } + if conf.fileNameLimit == 0 { + conf.fileNameLimit = defaultFileNameLimit } - s.lastMsgPos = 0 return nil } -func (s *LineSender) discardPendingMsg() { - s.buf.Truncate(s.lastMsgPos) - s.resetMsgFlags() -} - -func (s *LineSender) resetMsgFlags() { - s.hasTable = false - s.hasTags = false - s.hasFields = false -} - -// Messages returns a copy of accumulated ILP messages that are not -// flushed to the TCP connection yet. Useful for debugging purposes. -func (s *LineSender) Messages() string { - return s.buf.String() -} - -// buffer is a wrapper on top of bytes.buffer. It extends the -// original struct with methods for writing int64 and float64 -// numbers without unnecessary allocations. -type buffer struct { - bytes.Buffer -} +func validateConf(conf *lineSenderConfig) error { + if conf.initBufSize < 0 { + return fmt.Errorf("initial buffer size is negative: %d", conf.initBufSize) + } + if conf.maxBufSize < 0 { + return fmt.Errorf("max buffer size is negative: %d", conf.maxBufSize) + } -func newBuffer(cap int) *buffer { - return &buffer{*bytes.NewBuffer(make([]byte, 0, cap))} -} + if conf.fileNameLimit < 0 { + return fmt.Errorf("file name limit is negative: %d", conf.fileNameLimit) + } -func (b *buffer) WriteInt(i int64) { - // We need up to 20 bytes to fit an int64, including a sign. - var a [20]byte - s := strconv.AppendInt(a[0:0], i, 10) - b.Write(s) -} + if conf.retryTimeout < 0 { + return fmt.Errorf("retry timeout is negative: %d", conf.retryTimeout) + } + if conf.requestTimeout < 0 { + return fmt.Errorf("request timeout is negative: %d", conf.requestTimeout) + } + if conf.minThroughput < 0 { + return fmt.Errorf("min throughput is negative: %d", conf.minThroughput) + } -func (b *buffer) WriteFloat(f float64) { - if math.IsNaN(f) { - b.WriteString("NaN") - return - } else if math.IsInf(f, -1) { - b.WriteString("-Infinity") - return - } else if math.IsInf(f, 1) { - b.WriteString("Infinity") - return - } - // We need up to 24 bytes to fit a float64, including a sign. - var a [24]byte - s := strconv.AppendFloat(a[0:0], f, 'G', -1, 64) - b.Write(s) -} + if conf.autoFlushRows < 0 { + return fmt.Errorf("auto flush rows is negative: %d", conf.autoFlushRows) + } + if conf.autoFlushInterval < 0 { + return fmt.Errorf("auto flush interval is negative: %d", conf.autoFlushInterval) + } -func (b *buffer) WriteBigInt(i *big.Int) { - // We need up to 64 bytes to fit an unsigned 256-bit number. - var a [64]byte - s := i.Append(a[0:0], 16) - b.Write(s) + return nil } diff --git a/sender_integration_test.go b/sender_integration_test.go deleted file mode 100644 index ee8a72a..0000000 --- a/sender_integration_test.go +++ /dev/null @@ -1,743 +0,0 @@ -/******************************************************************************* - * ___ _ ____ ____ - * / _ \ _ _ ___ ___| |_| _ \| __ ) - * | | | | | | |/ _ \/ __| __| | | | _ \ - * | |_| | |_| | __/\__ \ |_| |_| | |_) | - * \__\_\\__,_|\___||___/\__|____/|____/ - * - * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2022 QuestDB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ******************************************************************************/ - -package questdb_test - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "math/big" - "net/http" - "net/url" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" - - qdb "github.com/questdb/go-questdb-client/v2" -) - -const ( - testTable = "my_test_table" - networkName = "test-network" - eventualDataTimeout = 60 * time.Second -) - -type questdbContainer struct { - testcontainers.Container - proxyC testcontainers.Container - network testcontainers.Network - httpAddress string - ilpAddress string - proxyIlpAddress string -} - -func (c *questdbContainer) Stop(ctx context.Context) error { - if c.proxyC != nil { - err := c.proxyC.Terminate(ctx) - if err != nil { - return err - } - } - err := c.Terminate(ctx) - if err != nil { - return err - } - err = c.network.Remove(ctx) - if err != nil { - return err - } - return nil -} - -type ilpAuthType int64 - -const ( - noAuth ilpAuthType = 0 - authEnabled ilpAuthType = 1 -) - -func setupQuestDB(ctx context.Context, auth ilpAuthType) (*questdbContainer, error) { - return setupQuestDB0(ctx, auth, false) -} - -func setupQuestDBWithProxy(ctx context.Context, auth ilpAuthType) (*questdbContainer, error) { - return setupQuestDB0(ctx, auth, true) -} - -func setupQuestDB0(ctx context.Context, auth ilpAuthType, setupProxy bool) (*questdbContainer, error) { - // Make sure that ingested rows are committed almost immediately. - env := map[string]string{ - "QDB_CAIRO_MAX_UNCOMMITTED_ROWS": "1", - "QDB_LINE_TCP_MAINTENANCE_JOB_INTERVAL": "100", - "QDB_PG_ENABLED": "false", - "QDB_HTTP_MIN_ENABLED": "false", - } - if auth == authEnabled { - env["QDB_LINE_TCP_AUTH_DB_PATH"] = "/auth/questdb.auth.txt" - } - - path, err := filepath.Abs("./test") - if err != nil { - return nil, err - } - req := testcontainers.ContainerRequest{ - Image: "questdb/questdb:7.3.2", - ExposedPorts: []string{"9000/tcp", "9009/tcp"}, - WaitingFor: wait.ForHTTP("/").WithPort("9000"), - Networks: []string{networkName}, - NetworkAliases: map[string][]string{networkName: {"questdb"}}, - Env: env, - Mounts: testcontainers.Mounts(testcontainers.ContainerMount{ - Source: testcontainers.GenericBindMountSource{ - HostPath: path, - }, - Target: testcontainers.ContainerMountTarget("/root/.questdb/auth"), - }), - } - - newNetwork, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ - NetworkRequest: testcontainers.NetworkRequest{ - Name: networkName, - CheckDuplicate: true, - }, - }) - if err != nil { - return nil, err - } - - qdbC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - if err != nil { - newNetwork.Remove(ctx) - return nil, err - } - - ip, err := qdbC.Host(ctx) - if err != nil { - newNetwork.Remove(ctx) - return nil, err - } - - mappedPort, err := qdbC.MappedPort(ctx, "9000") - if err != nil { - newNetwork.Remove(ctx) - return nil, err - } - httpAddress := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) - - mappedPort, err = qdbC.MappedPort(ctx, "9009") - if err != nil { - newNetwork.Remove(ctx) - return nil, err - } - ilpAddress := fmt.Sprintf("%s:%s", ip, mappedPort.Port()) - - var ( - haProxyC testcontainers.Container - proxyAddress string - ) - if setupProxy { - req = testcontainers.ContainerRequest{ - Image: "haproxy:2.6.0", - ExposedPorts: []string{"8443/tcp", "8888/tcp"}, - WaitingFor: wait.ForHTTP("/").WithPort("8888"), - Networks: []string{networkName}, - Mounts: testcontainers.Mounts(testcontainers.ContainerMount{ - Source: testcontainers.GenericBindMountSource{ - HostPath: path, - }, - Target: testcontainers.ContainerMountTarget("/usr/local/etc/haproxy"), - }), - } - haProxyC, err = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - if err != nil { - qdbC.Terminate(ctx) - newNetwork.Remove(ctx) - return nil, err - } - - ip, err := haProxyC.Host(ctx) - if err != nil { - qdbC.Terminate(ctx) - newNetwork.Remove(ctx) - return nil, err - } - - mappedPort, err := haProxyC.MappedPort(ctx, "8443") - if err != nil { - qdbC.Terminate(ctx) - newNetwork.Remove(ctx) - return nil, err - } - proxyAddress = fmt.Sprintf("%s:%s", ip, mappedPort.Port()) - } - - return &questdbContainer{ - Container: qdbC, - proxyC: haProxyC, - network: newNetwork, - httpAddress: httpAddress, - ilpAddress: ilpAddress, - proxyIlpAddress: proxyAddress, - }, nil -} - -func TestE2EValidWrites(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - - testCases := []struct { - name string - tableName string - writerFn writerFn - expected tableData - }{ - { - "all column types", - testTable, - func(s *qdb.LineSender) error { - val, _ := big.NewInt(0).SetString("123a4", 16) - err := s. - Table(testTable). - Symbol("sym_col", "test_ilp1"). - Float64Column("double_col", 12.2). - Int64Column("long_col", 12). - Long256Column("long256_col", val). - StringColumn("str_col", "foobar"). - BoolColumn("bool_col", true). - TimestampColumn("timestamp_col", time.UnixMicro(42)). - At(ctx, time.UnixMicro(1)) - if err != nil { - return err - } - - val, _ = big.NewInt(0).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) - return s. - Table(testTable). - Symbol("sym_col", "test_ilp2"). - Float64Column("double_col", 11.2). - Int64Column("long_col", 11). - Long256Column("long256_col", val). - StringColumn("str_col", "barbaz"). - BoolColumn("bool_col", false). - TimestampColumn("timestamp_col", time.UnixMicro(43)). - At(ctx, time.UnixMicro(2)) - }, - tableData{ - Columns: []column{ - {"sym_col", "SYMBOL"}, - {"double_col", "DOUBLE"}, - {"long_col", "LONG"}, - {"long256_col", "LONG256"}, - {"str_col", "STRING"}, - {"bool_col", "BOOLEAN"}, - {"timestamp_col", "TIMESTAMP"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"test_ilp1", float64(12.2), float64(12), "0x0123a4", "foobar", true, "1970-01-01T00:00:00.000042Z", "1970-01-01T00:00:00.000001Z"}, - {"test_ilp2", float64(11.2), float64(11), "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "barbaz", false, "1970-01-01T00:00:00.000043Z", "1970-01-01T00:00:00.000002Z"}, - }, - Count: 2, - }, - }, - { - "escaped chars", - "my-awesome_test 1=2.csv", - func(s *qdb.LineSender) error { - return s. - Table("my-awesome_test 1=2.csv"). - Symbol("sym_name 1=2", "value 1,2=3\n4\r5\"6\\7"). - StringColumn("str_name 1=2", "value 1,2=3\n4\r5\"6\\7"). - At(ctx, time.UnixMicro(1)) - }, - tableData{ - Columns: []column{ - {"sym_name 1=2", "SYMBOL"}, - {"str_name 1=2", "STRING"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"value 1,2=3\n4\r5\"6\\7", "value 1,2=3\n4\r5\"6\\7", "1970-01-01T00:00:00.000001Z"}, - }, - Count: 1, - }, - }, - { - "single symbol", - testTable, - func(s *qdb.LineSender) error { - return s. - Table(testTable). - Symbol("foo", "bar"). - At(ctx, time.UnixMicro(42)) - }, - tableData{ - Columns: []column{ - {"foo", "SYMBOL"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"bar", "1970-01-01T00:00:00.000042Z"}, - }, - Count: 1, - }, - }, - { - "single column", - testTable, - func(s *qdb.LineSender) error { - return s. - Table(testTable). - Int64Column("foobar", 1_000_042). - At(ctx, time.UnixMicro(42)) - }, - tableData{ - Columns: []column{ - {"foobar", "LONG"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {float64(1_000_042), "1970-01-01T00:00:00.000042Z"}, - }, - Count: 1, - }, - }, - { - "single column long256", - testTable, - func(s *qdb.LineSender) error { - val, _ := big.NewInt(0).SetString("7fffffffffffffff", 16) - return s. - Table(testTable). - Long256Column("foobar", val). - At(ctx, time.UnixMicro(42)) - }, - tableData{ - Columns: []column{ - {"foobar", "LONG256"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"0x7fffffffffffffff", "1970-01-01T00:00:00.000042Z"}, - }, - Count: 1, - }, - }, - { - "double value with exponent", - testTable, - func(s *qdb.LineSender) error { - return s. - Table(testTable). - Float64Column("foobar", 4.2e-100). - At(ctx, time.UnixMicro(1)) - }, - tableData{ - Columns: []column{ - {"foobar", "DOUBLE"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {4.2e-100, "1970-01-01T00:00:00.000001Z"}, - }, - Count: 1, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - questdbC, err := setupQuestDB(ctx, noAuth) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(questdbC.ilpAddress)) - assert.NoError(t, err) - - err = tc.writerFn(sender) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - assert.Eventually(t, func() bool { - data := queryTableData(t, tc.tableName, questdbC.httpAddress) - return reflect.DeepEqual(tc.expected, data) - }, eventualDataTimeout, 100*time.Millisecond) - - sender.Close() - questdbC.Stop(ctx) - }) - } -} - -func TestE2EWriteInBatches(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - const ( - n = 100 - nBatch = 100 - ) - - ctx := context.Background() - - questdbC, err := setupQuestDB(ctx, noAuth) - assert.NoError(t, err) - defer questdbC.Stop(ctx) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(questdbC.ilpAddress)) - assert.NoError(t, err) - defer sender.Close() - - for i := 0; i < n; i++ { - for j := 0; j < nBatch; j++ { - err = sender. - Table(testTable). - Int64Column("long_col", int64(j)). - At(ctx, time.UnixMicro(int64(i*nBatch+j))) - assert.NoError(t, err) - } - err = sender.Flush(ctx) - assert.NoError(t, err) - } - - expected := tableData{ - Columns: []column{ - {"long_col", "LONG"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{}, - Count: n * nBatch, - } - - for i := 0; i < n; i++ { - for j := 0; j < nBatch; j++ { - expected.Dataset = append( - expected.Dataset, - []interface{}{float64(j), "1970-01-01T00:00:00." + fmt.Sprintf("%06d", i*nBatch+j) + "Z"}, - ) - } - } - - assert.Eventually(t, func() bool { - data := queryTableData(t, testTable, questdbC.httpAddress) - return reflect.DeepEqual(expected, data) - }, eventualDataTimeout, 100*time.Millisecond) -} - -func TestE2EImplicitFlush(t *testing.T) { - const bufCap = 100 - - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - - questdbC, err := setupQuestDB(ctx, noAuth) - assert.NoError(t, err) - defer questdbC.Stop(ctx) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(questdbC.ilpAddress), qdb.WithBufferCapacity(bufCap)) - assert.NoError(t, err) - defer sender.Close() - - for i := 0; i < 10*bufCap; i++ { - err = sender. - Table(testTable). - BoolColumn("b", true). - AtNow(ctx) - assert.NoError(t, err) - } - - assert.Eventually(t, func() bool { - data := queryTableData(t, testTable, questdbC.httpAddress) - // We didn't call Flush, but we expect the buffer to be flushed at least once. - return data.Count > 0 - }, eventualDataTimeout, 100*time.Millisecond) -} - -func TestE2ESuccessfulAuth(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - - questdbC, err := setupQuestDB(ctx, authEnabled) - assert.NoError(t, err) - defer questdbC.Stop(ctx) - - sender, err := qdb.NewLineSender( - ctx, - qdb.WithAddress(questdbC.ilpAddress), - qdb.WithAuth("testUser1", "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48"), - ) - assert.NoError(t, err) - - err = sender. - Table(testTable). - StringColumn("str_col", "foobar"). - At(ctx, time.UnixMicro(1)) - assert.NoError(t, err) - - err = sender. - Table(testTable). - StringColumn("str_col", "barbaz"). - At(ctx, time.UnixMicro(2)) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - // Close the connection to make sure that ILP messages are written. That's because - // the server may not write messages that are received immediately after the signed - // challenge until the connection is closed or more data is received. - sender.Close() - - expected := tableData{ - Columns: []column{ - {"str_col", "STRING"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"foobar", "1970-01-01T00:00:00.000001Z"}, - {"barbaz", "1970-01-01T00:00:00.000002Z"}, - }, - Count: 2, - } - - assert.Eventually(t, func() bool { - data := queryTableData(t, testTable, questdbC.httpAddress) - return reflect.DeepEqual(expected, data) - }, eventualDataTimeout, 100*time.Millisecond) -} - -func TestE2EFailedAuth(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - - questdbC, err := setupQuestDB(ctx, authEnabled) - assert.NoError(t, err) - defer questdbC.Stop(ctx) - - sender, err := qdb.NewLineSender( - ctx, - qdb.WithAddress(questdbC.ilpAddress), - qdb.WithAuth("wrongKeyId", "1234567890"), - ) - assert.NoError(t, err) - defer sender.Close() - - err = sender. - Table(testTable). - StringColumn("str_col", "foobar"). - At(ctx, time.UnixMicro(1)) - // If we get an error here or later, it means that the server closed connection. - if err != nil { - return - } - - err = sender. - Table(testTable). - StringColumn("str_col", "barbaz"). - At(ctx, time.UnixMicro(2)) - if err != nil { - return - } - - err = sender.Flush(ctx) - if err != nil { - return - } - - // Our writes should not get applied. - time.Sleep(2 * time.Second) - data := queryTableData(t, testTable, questdbC.httpAddress) - assert.Equal(t, 0, data.Count) -} - -func TestE2EWritesWithTlsProxy(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - - questdbC, err := setupQuestDBWithProxy(ctx, noAuth) - assert.NoError(t, err) - defer questdbC.Stop(ctx) - - sender, err := qdb.NewLineSender( - ctx, - qdb.WithAddress(questdbC.proxyIlpAddress), // We're sending data through proxy. - qdb.WithTlsInsecureSkipVerify(), - ) - assert.NoError(t, err) - defer sender.Close() - - err = sender. - Table(testTable). - StringColumn("str_col", "foobar"). - At(ctx, time.UnixMicro(1)) - assert.NoError(t, err) - - err = sender. - Table(testTable). - StringColumn("str_col", "barbaz"). - At(ctx, time.UnixMicro(2)) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - expected := tableData{ - Columns: []column{ - {"str_col", "STRING"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"foobar", "1970-01-01T00:00:00.000001Z"}, - {"barbaz", "1970-01-01T00:00:00.000002Z"}, - }, - Count: 2, - } - - assert.Eventually(t, func() bool { - data := queryTableData(t, testTable, questdbC.httpAddress) - return reflect.DeepEqual(expected, data) - }, eventualDataTimeout, 100*time.Millisecond) -} - -func TestE2ESuccessfulAuthWithTlsProxy(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - - questdbC, err := setupQuestDBWithProxy(ctx, authEnabled) - assert.NoError(t, err) - defer questdbC.Stop(ctx) - - sender, err := qdb.NewLineSender( - ctx, - qdb.WithAddress(questdbC.proxyIlpAddress), // We're sending data through proxy. - qdb.WithAuth("testUser1", "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48"), - qdb.WithTlsInsecureSkipVerify(), - ) - assert.NoError(t, err) - - err = sender. - Table(testTable). - StringColumn("str_col", "foobar"). - At(ctx, time.UnixMicro(1)) - assert.NoError(t, err) - - err = sender. - Table(testTable). - StringColumn("str_col", "barbaz"). - At(ctx, time.UnixMicro(2)) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - // Close the connection to make sure that ILP messages are written. That's because - // the server may not write messages that are received immediately after the signed - // challenge until the connection is closed or more data is received. - sender.Close() - - expected := tableData{ - Columns: []column{ - {"str_col", "STRING"}, - {"timestamp", "TIMESTAMP"}, - }, - Dataset: [][]interface{}{ - {"foobar", "1970-01-01T00:00:00.000001Z"}, - {"barbaz", "1970-01-01T00:00:00.000002Z"}, - }, - Count: 2, - } - - assert.Eventually(t, func() bool { - data := queryTableData(t, testTable, questdbC.httpAddress) - return reflect.DeepEqual(expected, data) - }, eventualDataTimeout, 100*time.Millisecond) -} - -type tableData struct { - Columns []column `json:"columns"` - Dataset [][]interface{} `json:"dataset"` - Count int `json:"count"` -} - -type column struct { - Name string `json:"name"` - Type string `json:"type"` -} - -func queryTableData(t *testing.T, tableName, address string) tableData { - u, err := url.Parse(address) - assert.NoError(t, err) - - u.Path += "exec" - params := url.Values{} - params.Add("query", "'"+tableName+"'") - u.RawQuery = params.Encode() - url := fmt.Sprintf("%v", u) - - res, err := http.Get(url) - assert.NoError(t, err) - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - assert.NoError(t, err) - - data := tableData{} - err = json.Unmarshal(body, &data) - assert.NoError(t, err) - - return data -} diff --git a/sender_test.go b/sender_test.go deleted file mode 100644 index 7a55b33..0000000 --- a/sender_test.go +++ /dev/null @@ -1,787 +0,0 @@ -/******************************************************************************* - * ___ _ ____ ____ - * / _ \ _ _ ___ ___| |_| _ \| __ ) - * | | | | | | |/ _ \/ __| __| | | | _ \ - * | |_| | |_| | __/\__ \ |_| |_| | |_) | - * \__\_\\__,_|\___||___/\__|____/|____/ - * - * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2022 QuestDB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ******************************************************************************/ - -package questdb_test - -import ( - "bufio" - "context" - "fmt" - "io" - "io/ioutil" - "log" - "math" - "math/big" - "net" - "reflect" - "strconv" - "strings" - "sync" - "testing" - "time" - - qdb "github.com/questdb/go-questdb-client/v2" - "github.com/stretchr/testify/assert" -) - -type writerFn func(s *qdb.LineSender) error - -func TestValidWrites(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - writerFn writerFn - expectedLines []string - }{ - { - "multiple rows", - func(s *qdb.LineSender) error { - err := s.Table(testTable).StringColumn("str_col", "foo").Int64Column("long_col", 42).AtNow(ctx) - if err != nil { - return err - } - err = s.Table(testTable).StringColumn("str_col", "bar").Int64Column("long_col", -42).At(ctx, time.UnixMicro(42)) - if err != nil { - return err - } - return nil - }, - []string{ - "my_test_table str_col=\"foo\",long_col=42i", - "my_test_table str_col=\"bar\",long_col=-42i 42000", - }, - }, - { - "UTF-8 strings", - func(s *qdb.LineSender) error { - return s.Table("таблица").StringColumn("колонка", "значение").AtNow(ctx) - }, - []string{ - "таблица колонка=\"значение\"", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(sendToBackChannel) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - err = tc.writerFn(sender) - assert.NoError(t, err) - - // Check the buffer before flushing it. - assert.Equal(t, strings.Join(tc.expectedLines, "\n")+"\n", sender.Messages()) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - sender.Close() - - // Now check what was received by the server. - expectLines(t, srv.backCh, tc.expectedLines) - - srv.close() - }) - } -} - -func TestTimestampSerialization(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - val time.Time - }{ - {"max value", time.UnixMicro(math.MaxInt64)}, - {"zero", time.UnixMicro(0)}, - {"small positive value", time.UnixMicro(10)}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(sendToBackChannel) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - err = sender.Table(testTable).TimestampColumn("a_col", tc.val).AtNow(ctx) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - sender.Close() - - // Now check what was received by the server. - expectLines(t, srv.backCh, []string{"my_test_table a_col=" + strconv.FormatInt(tc.val.UnixMicro(), 10) + "t"}) - - srv.close() - }) - } -} - -func TestInt64Serialization(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - val int64 - }{ - {"min value", math.MinInt64}, - {"max value", math.MaxInt64}, - {"zero", 0}, - {"small negative value", -10}, - {"small positive value", 10}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(sendToBackChannel) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - err = sender.Table(testTable).Int64Column("a_col", tc.val).AtNow(ctx) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - sender.Close() - - // Now check what was received by the server. - expectLines(t, srv.backCh, []string{"my_test_table a_col=" + strconv.FormatInt(tc.val, 10) + "i"}) - - srv.close() - }) - } -} - -func TestLong256Column(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - val string - expected string - }{ - {"zero", "0", "0x0"}, - {"one", "1", "0x1"}, - {"32-bit max", strconv.FormatInt(math.MaxInt32, 16), "0x7fffffff"}, - {"64-bit random", strconv.FormatInt(7423093023234231, 16), "0x1a5f4386c8d8b7"}, - {"256-bit max", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(sendToBackChannel) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - newVal, _ := big.NewInt(0).SetString(tc.val, 16) - err = sender.Table(testTable).Long256Column("a_col", newVal).AtNow(ctx) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - sender.Close() - - // Now check what was received by the server. - expectLines(t, srv.backCh, []string{"my_test_table a_col=" + tc.expected + "i"}) - - srv.close() - }) - } -} - -func TestFloat64Serialization(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - val float64 - expected string - }{ - {"NaN", math.NaN(), "NaN"}, - {"positive infinity", math.Inf(1), "Infinity"}, - {"negative infinity", math.Inf(-1), "-Infinity"}, - {"negative infinity", math.Inf(-1), "-Infinity"}, - {"positive number", 42.3, "42.3"}, - {"negative number", -42.3, "-42.3"}, - {"smallest value", math.SmallestNonzeroFloat64, "5E-324"}, - {"max value", math.MaxFloat64, "1.7976931348623157E+308"}, - {"negative with exponent", -4.2e-99, "-4.2E-99"}, - {"small with exponent", 4.2e-99, "4.2E-99"}, - {"large with exponent", 4.2e99, "4.2E+99"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(sendToBackChannel) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - err = sender.Table(testTable).Float64Column("a_col", tc.val).AtNow(ctx) - assert.NoError(t, err) - - err = sender.Flush(ctx) - assert.NoError(t, err) - - sender.Close() - - // Now check what was received by the server. - expectLines(t, srv.backCh, []string{"my_test_table a_col=" + tc.expected}) - - srv.close() - }) - } -} - -func TestErrorOnLengthyNames(t *testing.T) { - const nameLimit = 42 - - var ( - lengthyStr = strings.Repeat("a", nameLimit+1) - ctx = context.Background() - ) - - testCases := []struct { - name string - writerFn writerFn - expectedErrMsg string - }{ - { - "lengthy table name", - func(s *qdb.LineSender) error { - return s.Table(lengthyStr).StringColumn("str_col", "foo").AtNow(ctx) - }, - "table name length exceeds the limit", - }, - { - "lengthy column name", - func(s *qdb.LineSender) error { - return s.Table(testTable).StringColumn(lengthyStr, "foo").AtNow(ctx) - }, - "column name length exceeds the limit", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr), qdb.WithFileNameLimit(nameLimit)) - assert.NoError(t, err) - - err = tc.writerFn(sender) - assert.ErrorContains(t, err, tc.expectedErrMsg) - assert.Empty(t, sender.Messages()) - - sender.Close() - srv.close() - }) - } -} - -func TestErrorOnMissingTableCall(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - writerFn writerFn - }{ - { - "AtNow", - func(s *qdb.LineSender) error { - return s.Symbol("sym", "abc").AtNow(ctx) - }, - }, - { - "At", - func(s *qdb.LineSender) error { - return s.Symbol("sym", "abc").At(ctx, time.UnixMicro(0)) - }, - }, - { - "symbol", - func(s *qdb.LineSender) error { - return s.Symbol("sym", "abc").AtNow(ctx) - }, - }, - { - "string column", - func(s *qdb.LineSender) error { - return s.StringColumn("str", "abc").AtNow(ctx) - }, - }, - { - "boolean column", - func(s *qdb.LineSender) error { - return s.BoolColumn("bool", true).AtNow(ctx) - }, - }, - { - "long column", - func(s *qdb.LineSender) error { - return s.Int64Column("int", 42).AtNow(ctx) - }, - }, - { - "double column", - func(s *qdb.LineSender) error { - return s.Float64Column("float", 4.2).AtNow(ctx) - }, - }, - { - "timestamp column", - func(s *qdb.LineSender) error { - return s.TimestampColumn("timestamp", time.UnixMicro(42)).AtNow(ctx) - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - err = tc.writerFn(sender) - - assert.ErrorContains(t, err, "table name was not provided") - assert.Empty(t, sender.Messages()) - - sender.Close() - srv.close() - }) - } -} - -func TestErrorOnMultipleTableCalls(t *testing.T) { - ctx := context.Background() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - err = sender.Table(testTable).Table(testTable).AtNow(ctx) - - assert.ErrorContains(t, err, "table name already provided") - assert.Empty(t, sender.Messages()) -} - -func TestErrorOnNegativeLong256(t *testing.T) { - ctx := context.Background() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - err = sender.Table(testTable).Long256Column("long256_col", big.NewInt(-42)).AtNow(ctx) - - assert.ErrorContains(t, err, "long256 cannot be negative: -42") - assert.Empty(t, sender.Messages()) -} - -func TestErrorOnLargerLong256(t *testing.T) { - ctx := context.Background() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - bigVal, _ := big.NewInt(0).SetString("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) - err = sender.Table(testTable).Long256Column("long256_col", bigVal).AtNow(ctx) - - assert.ErrorContains(t, err, "long256 cannot be larger than 256-bit: 260") - assert.Empty(t, sender.Messages()) -} - -func TestErrorOnSymbolCallAfterColumn(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - writerFn writerFn - }{ - { - "string column", - func(s *qdb.LineSender) error { - return s.Table("awesome_table").StringColumn("str", "abc").Symbol("sym", "abc").AtNow(ctx) - }, - }, - { - "boolean column", - func(s *qdb.LineSender) error { - return s.Table("awesome_table").BoolColumn("bool", true).Symbol("sym", "abc").AtNow(ctx) - }, - }, - { - "integer column", - func(s *qdb.LineSender) error { - return s.Table("awesome_table").Int64Column("int", 42).Symbol("sym", "abc").AtNow(ctx) - }, - }, - { - "float column", - func(s *qdb.LineSender) error { - return s.Table("awesome_table").Float64Column("float", 4.2).Symbol("sym", "abc").AtNow(ctx) - }, - }, - { - "timestamp column", - func(s *qdb.LineSender) error { - return s.Table("awesome_table").TimestampColumn("timestamp", time.UnixMicro(42)).Symbol("sym", "abc").AtNow(ctx) - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - - err = tc.writerFn(sender) - - assert.ErrorContains(t, err, "symbols have to be written before any other column") - assert.Empty(t, sender.Messages()) - - sender.Close() - srv.close() - }) - } -} - -func TestErrorOnFlushWhenMessageIsPending(t *testing.T) { - ctx := context.Background() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - sender.Table(testTable) - err = sender.Flush(ctx) - - assert.ErrorContains(t, err, "pending ILP message must be finalized with At or AtNow before calling Flush") - assert.Empty(t, sender.Messages()) -} - -func TestInvalidMessageGetsDiscarded(t *testing.T) { - ctx := context.Background() - - srv, err := newTestServer(sendToBackChannel) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - // Write a valid message. - err = sender.Table(testTable).StringColumn("foo", "bar").AtNow(ctx) - assert.NoError(t, err) - // Then write perform an incorrect chain of calls. - err = sender.Table(testTable).StringColumn("foo", "bar").Symbol("sym", "42").AtNow(ctx) - assert.Error(t, err) - - // The second message should be discarded. - err = sender.Flush(ctx) - assert.NoError(t, err) - expectLines(t, srv.backCh, []string{testTable + " foo=\"bar\""}) -} - -func TestErrorOnUnavailableServer(t *testing.T) { - ctx := context.Background() - - _, err := qdb.NewLineSender(ctx) - assert.ErrorContains(t, err, "failed to connect to server") -} - -func TestErrorOnCancelledContext(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - // The context is not cancelled yet, so Flush should succeed. - err = sender.Table(testTable).StringColumn("foo", "bar").AtNow(ctx) - assert.NoError(t, err) - err = sender.Flush(ctx) - assert.NoError(t, err) - - cancel() - - // The context is now cancelled, so we expect an error. - err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) - assert.NoError(t, err) - err = sender.Flush(ctx) - assert.Error(t, err) -} - -func TestErrorOnContextDeadline(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(50*time.Millisecond)) - defer cancel() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(t, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(t, err) - defer sender.Close() - - // Keep writing until we get an error due to the context deadline. - for i := 0; i < 100_000; i++ { - err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) - if err != nil { - return - } - err = sender.Flush(ctx) - if err != nil { - return - } - time.Sleep(5 * time.Millisecond) - } - t.Fail() -} - -func BenchmarkLineSenderBatch1000(b *testing.B) { - ctx := context.Background() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(b, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(b, err) - defer sender.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - for j := 0; j < 1000; j++ { - sender. - Table(testTable). - Symbol("sym_col", "test_ilp1"). - Float64Column("double_col", float64(i)+0.42). - Int64Column("long_col", int64(i)). - StringColumn("str_col", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"). - BoolColumn("bool_col", true). - TimestampColumn("timestamp_col", time.UnixMicro(42)). - At(ctx, time.UnixMicro(int64(1000*i))) - } - sender.Flush(ctx) - } -} - -func BenchmarkLineSenderNoFlush(b *testing.B) { - ctx := context.Background() - - srv, err := newTestServer(readAndDiscard) - assert.NoError(b, err) - defer srv.close() - - sender, err := qdb.NewLineSender(ctx, qdb.WithAddress(srv.addr)) - assert.NoError(b, err) - defer sender.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - sender. - Table(testTable). - Symbol("sym_col", "test_ilp1"). - Float64Column("double_col", float64(i)+0.42). - Int64Column("long_col", int64(i)). - StringColumn("str_col", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"). - BoolColumn("bool_col", true). - TimestampColumn("timestamp_col", time.UnixMicro(42)). - At(ctx, time.UnixMicro(int64(1000*i))) - } - sender.Flush(ctx) -} - -func expectLines(t *testing.T, linesCh chan string, expected []string) { - actual := make([]string, 0) - assert.Eventually(t, func() bool { - select { - case l := <-linesCh: - actual = append(actual, l) - default: - return false - } - return reflect.DeepEqual(expected, actual) - }, 3*time.Second, 100*time.Millisecond) -} - -type serverType int64 - -const ( - sendToBackChannel serverType = 0 - readAndDiscard serverType = 1 -) - -type testServer struct { - addr string - listener net.Listener - serverType serverType - backCh chan string - closeCh chan struct{} - wg sync.WaitGroup -} - -func newTestServer(serverType serverType) (*testServer, error) { - tcp, err := net.Listen("tcp", "127.0.0.1:") - if err != nil { - return nil, err - } - s := &testServer{ - addr: tcp.Addr().String(), - listener: tcp, - serverType: serverType, - backCh: make(chan string), - closeCh: make(chan struct{}), - } - s.wg.Add(1) - go s.serve() - return s, nil -} - -func (s *testServer) serve() { - defer s.wg.Done() - - for { - conn, err := s.listener.Accept() - if err != nil { - select { - case <-s.closeCh: - return - default: - log.Println("could not accept", err) - } - continue - } - - s.wg.Add(1) - go func() { - switch s.serverType { - case sendToBackChannel: - s.handleSendToBackChannel(conn) - case readAndDiscard: - s.handleReadAndDiscard(conn) - default: - panic(fmt.Sprintf("server type is not supported: %d", s.serverType)) - } - s.wg.Done() - }() - } -} - -func (s *testServer) handleSendToBackChannel(conn net.Conn) { - defer conn.Close() - - r := bufio.NewReader(conn) - for { - select { - case <-s.closeCh: - return - default: - l, err := r.ReadString('\n') - if err != nil { - if err == io.EOF { - continue - } else { - log.Println("could not read", err) - return - } - } - // Remove trailing \n and send line to back channel. - s.backCh <- l[0 : len(l)-1] - } - } -} - -func (s *testServer) handleReadAndDiscard(conn net.Conn) { - defer conn.Close() - - for { - select { - case <-s.closeCh: - return - default: - _, err := io.Copy(ioutil.Discard, conn) - if err != nil { - if err == io.EOF { - continue - } else { - log.Println("could not read", err) - return - } - } - } - } -} - -func (s *testServer) close() { - close(s.closeCh) - s.listener.Close() - s.wg.Wait() -} diff --git a/tcp_integration_test.go b/tcp_integration_test.go new file mode 100644 index 0000000..2b0f3c3 --- /dev/null +++ b/tcp_integration_test.go @@ -0,0 +1,382 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + qdb "github.com/questdb/go-questdb-client/v3" +) + +func (suite *integrationTestSuite) TestE2EWriteInBatches() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + const ( + n = 100 + nBatch = 100 + ) + + ctx := context.Background() + + questdbC, err := setupQuestDB(ctx, noAuth) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(questdbC.ilpAddress)) + assert.NoError(suite.T(), err) + defer sender.Close(ctx) + + for i := 0; i < n; i++ { + for j := 0; j < nBatch; j++ { + err = sender. + Table(testTable). + Int64Column("long_col", int64(j)). + At(ctx, time.UnixMicro(int64(i*nBatch+j))) + assert.NoError(suite.T(), err) + } + err = sender.Flush(ctx) + assert.NoError(suite.T(), err) + } + + expected := tableData{ + Columns: []column{ + {"long_col", "LONG"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{}, + Count: n * nBatch, + } + + for i := 0; i < n; i++ { + for j := 0; j < nBatch; j++ { + expected.Dataset = append( + expected.Dataset, + []interface{}{float64(j), "1970-01-01T00:00:00." + fmt.Sprintf("%06d", i*nBatch+j) + "Z"}, + ) + } + } + + assert.Eventually(suite.T(), func() bool { + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + return reflect.DeepEqual(expected, data) + }, eventualDataTimeout, 100*time.Millisecond) +} + +func (suite *integrationTestSuite) TestE2EImplicitFlush() { + const bufCap = 100 + + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + questdbC, err := setupQuestDB(ctx, noAuth) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(questdbC.ilpAddress), qdb.WithInitBufferSize(bufCap)) + assert.NoError(suite.T(), err) + defer sender.Close(ctx) + + for i := 0; i < 10*bufCap; i++ { + err = sender. + Table(testTable). + BoolColumn("b", true). + AtNow(ctx) + assert.NoError(suite.T(), err) + } + + assert.Eventually(suite.T(), func() bool { + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + // We didn't call Flush, but we expect the buffer to be flushed at least once. + return data.Count > 0 + }, eventualDataTimeout, 100*time.Millisecond) +} + +func (suite *integrationTestSuite) TestE2ESuccessfulAuth() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + questdbC, err := setupQuestDB(ctx, authEnabled) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithTcp(), + qdb.WithAddress(questdbC.ilpAddress), + qdb.WithAuth("testUser1", "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48"), + ) + assert.NoError(suite.T(), err) + + err = sender. + Table(testTable). + StringColumn("str_col", "foobar"). + At(ctx, time.UnixMicro(1)) + assert.NoError(suite.T(), err) + + err = sender. + Table(testTable). + StringColumn("str_col", "barbaz"). + At(ctx, time.UnixMicro(2)) + assert.NoError(suite.T(), err) + + err = sender.Flush(ctx) + assert.NoError(suite.T(), err) + + // Close the connection to make sure that ILP messages are written. That's because + // the server may not write messages that are received immediately after the signed + // challenge until the connection is closed or more data is received. + sender.Close(ctx) + + expected := tableData{ + Columns: []column{ + {"str_col", "STRING"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"foobar", "1970-01-01T00:00:00.000001Z"}, + {"barbaz", "1970-01-01T00:00:00.000002Z"}, + }, + Count: 2, + } + + assert.Eventually(suite.T(), func() bool { + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + return reflect.DeepEqual(expected, data) + }, eventualDataTimeout, 100*time.Millisecond) +} + +func (suite *integrationTestSuite) TestE2EFailedAuth() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + questdbC, err := setupQuestDB(ctx, authEnabled) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithTcp(), + qdb.WithAddress(questdbC.ilpAddress), + qdb.WithAuth("wrongKeyId", "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48"), + ) + assert.NoError(suite.T(), err) + defer sender.Close(ctx) + + err = sender. + Table(testTable). + StringColumn("str_col", "foobar"). + At(ctx, time.UnixMicro(1)) + // If we get an error here or later, it means that the server closed connection. + if err != nil { + return + } + + err = sender. + Table(testTable). + StringColumn("str_col", "barbaz"). + At(ctx, time.UnixMicro(2)) + if err != nil { + return + } + + err = sender.Flush(ctx) + if err != nil { + return + } + + // Our writes should not get applied. + time.Sleep(2 * time.Second) + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + assert.Equal(suite.T(), 0, data.Count) +} + +func (suite *integrationTestSuite) TestE2EWritesWithTlsProxy() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + questdbC, err := setupQuestDBWithProxy(ctx, noAuth) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithTcp(), + qdb.WithAddress(questdbC.proxyIlpTcpAddress), // We're sending data through proxy. + qdb.WithTlsInsecureSkipVerify(), + ) + assert.NoError(suite.T(), err) + defer sender.Close(ctx) + + err = sender. + Table(testTable). + StringColumn("str_col", "foobar"). + At(ctx, time.UnixMicro(1)) + assert.NoError(suite.T(), err) + + err = sender. + Table(testTable). + StringColumn("str_col", "barbaz"). + At(ctx, time.UnixMicro(2)) + assert.NoError(suite.T(), err) + + err = sender.Flush(ctx) + assert.NoError(suite.T(), err) + + expected := tableData{ + Columns: []column{ + {"str_col", "STRING"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"foobar", "1970-01-01T00:00:00.000001Z"}, + {"barbaz", "1970-01-01T00:00:00.000002Z"}, + }, + Count: 2, + } + + assert.Eventually(suite.T(), func() bool { + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + return reflect.DeepEqual(expected, data) + }, eventualDataTimeout, 100*time.Millisecond) +} + +func (suite *integrationTestSuite) TestE2ESuccessfulAuthWithTlsProxy() { + if testing.Short() { + suite.T().Skip("skipping integration test") + } + + ctx := context.Background() + + questdbC, err := setupQuestDBWithProxy(ctx, authEnabled) + assert.NoError(suite.T(), err) + defer questdbC.Stop(ctx) + + sender, err := qdb.NewLineSender( + ctx, + qdb.WithTcp(), + qdb.WithAddress(questdbC.proxyIlpTcpAddress), // We're sending data through proxy. + qdb.WithAuth("testUser1", "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48"), + qdb.WithTlsInsecureSkipVerify(), + ) + assert.NoError(suite.T(), err) + + err = sender. + Table(testTable). + StringColumn("str_col", "foobar"). + At(ctx, time.UnixMicro(1)) + assert.NoError(suite.T(), err) + + err = sender. + Table(testTable). + StringColumn("str_col", "barbaz"). + At(ctx, time.UnixMicro(2)) + assert.NoError(suite.T(), err) + + err = sender.Flush(ctx) + assert.NoError(suite.T(), err) + + // Close the connection to make sure that ILP messages are written. That's because + // the server may not write messages that are received immediately after the signed + // challenge until the connection is closed or more data is received. + sender.Close(ctx) + + expected := tableData{ + Columns: []column{ + {"str_col", "STRING"}, + {"timestamp", "TIMESTAMP"}, + }, + Dataset: [][]interface{}{ + {"foobar", "1970-01-01T00:00:00.000001Z"}, + {"barbaz", "1970-01-01T00:00:00.000002Z"}, + }, + Count: 2, + } + + assert.Eventually(suite.T(), func() bool { + data := queryTableData(suite.T(), testTable, questdbC.httpAddress) + return reflect.DeepEqual(expected, data) + }, eventualDataTimeout, 100*time.Millisecond) +} + +type tableData struct { + Columns []column `json:"columns"` + Dataset [][]interface{} `json:"dataset"` + Count int `json:"count"` +} + +type column struct { + Name string `json:"name"` + Type string `json:"type"` +} + +func queryTableData(t *testing.T, tableName, address string) tableData { + // We always query data using the QuestDB container over http + address = "http://" + address + u, err := url.Parse(address) + assert.NoError(t, err) + + u.Path += "exec" + params := url.Values{} + params.Add("query", "'"+tableName+"'") + u.RawQuery = params.Encode() + url := fmt.Sprintf("%v", u) + + res, err := http.Get(url) + assert.NoError(t, err) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + + data := tableData{} + err = json.Unmarshal(body, &data) + assert.NoError(t, err) + + return data +} diff --git a/tcp_sender.go b/tcp_sender.go new file mode 100644 index 0000000..ffde51d --- /dev/null +++ b/tcp_sender.go @@ -0,0 +1,266 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb + +import ( + "bufio" + "context" + "crypto" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "math/big" + "net" + "time" +) + +type tcpLineSender struct { + buf buffer + address string + conn net.Conn +} + +func newTcpLineSender(ctx context.Context, conf *lineSenderConfig) (*tcpLineSender, error) { + var ( + d net.Dialer + key *ecdsa.PrivateKey + conn net.Conn + err error + ) + + s := &tcpLineSender{ + address: conf.address, + // TCP sender doesn't limit max buffer size, hence 0 + buf: newBuffer(conf.initBufSize, 0, conf.fileNameLimit), + } + + // Process tcp args in the same exact way that we do in v2 + if conf.tcpKeyId != "" && conf.tcpKey != "" { + rawKey, err := base64.RawURLEncoding.DecodeString(conf.tcpKey) + if err != nil { + return nil, fmt.Errorf("failed to decode auth key: %v", err) + } + // elliptic.P256().ScalarBaseMult is deprecated, so we use ecdh key + // and convert it to the ecdsa one. + ecdhKey, err := ecdh.P256().NewPrivateKey(rawKey) + if err != nil { + return nil, fmt.Errorf("invalid auth key: %v", err) + } + ecdhPubKey := ecdhKey.PublicKey().Bytes() + key = &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: big.NewInt(0).SetBytes(ecdhPubKey[1:33]), + Y: big.NewInt(0).SetBytes(ecdhPubKey[33:]), + }, + D: big.NewInt(0).SetBytes(ecdhKey.Bytes()), + } + } + + if conf.tlsMode == tlsDisabled { + conn, err = d.DialContext(ctx, "tcp", s.address) + } else { + config := &tls.Config{} + if conf.tlsMode == tlsInsecureSkipVerify { + config.InsecureSkipVerify = true + } + conn, err = tls.DialWithDialer(&d, "tcp", s.address, config) + } + if err != nil { + return nil, fmt.Errorf("failed to connect to server: %v", err) + } + + if key != nil { + if deadline, ok := ctx.Deadline(); ok { + conn.SetDeadline(deadline) + } + + _, err = conn.Write([]byte(conf.tcpKeyId + "\n")) + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to write key id: %v", err) + } + + reader := bufio.NewReader(conn) + raw, err := reader.ReadBytes('\n') + if len(raw) < 2 { + conn.Close() + return nil, fmt.Errorf("empty challenge response from server: %v", err) + } + // Remove the `\n` in the last position. + raw = raw[:len(raw)-1] + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to read challenge response from server: %v", err) + } + + // Hash the challenge with sha256. + hash := crypto.SHA256.New() + hash.Write(raw) + hashed := hash.Sum(nil) + + stdSig, err := ecdsa.SignASN1(rand.Reader, key, hashed) + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to sign challenge using auth key: %v", err) + } + _, err = conn.Write([]byte(base64.StdEncoding.EncodeToString(stdSig) + "\n")) + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to write signed challenge: %v", err) + } + + // Reset the deadline. + conn.SetDeadline(time.Time{}) + } + + s.conn = conn + + return s, nil +} + +func (s *tcpLineSender) Close(_ context.Context) error { + if s.conn != nil { + conn := s.conn + s.conn = nil + return conn.Close() + } + return nil +} + +func (s *tcpLineSender) Table(name string) LineSender { + s.buf.Table(name) + return s +} + +func (s *tcpLineSender) Symbol(name, val string) LineSender { + s.buf.Symbol(name, val) + return s +} + +func (s *tcpLineSender) Int64Column(name string, val int64) LineSender { + s.buf.Int64Column(name, val) + return s +} + +func (s *tcpLineSender) Long256Column(name string, val *big.Int) LineSender { + s.buf.Long256Column(name, val) + return s +} + +func (s *tcpLineSender) TimestampColumn(name string, ts time.Time) LineSender { + s.buf.TimestampColumn(name, ts) + return s +} + +func (s *tcpLineSender) Float64Column(name string, val float64) LineSender { + s.buf.Float64Column(name, val) + return s +} + +func (s *tcpLineSender) StringColumn(name, val string) LineSender { + s.buf.StringColumn(name, val) + return s +} + +func (s *tcpLineSender) BoolColumn(name string, val bool) LineSender { + s.buf.BoolColumn(name, val) + return s +} + +func (s *tcpLineSender) Flush(ctx context.Context) error { + err := s.buf.LastErr() + s.buf.ClearLastErr() + if err != nil { + s.buf.DiscardPendingMsg() + return err + } + if s.buf.HasTable() { + s.buf.DiscardPendingMsg() + return errors.New("pending ILP message must be finalized with At or AtNow before calling Flush") + } + + if err = ctx.Err(); err != nil { + return err + } + if deadline, ok := ctx.Deadline(); ok { + s.conn.SetWriteDeadline(deadline) + } else { + s.conn.SetWriteDeadline(time.Time{}) + } + + if _, err := s.buf.WriteTo(s.conn); err != nil { + return err + } + + // bytes.Buffer grows as 2*cap+n, so we use 3x as the threshold. + if s.buf.Cap() > 3*s.buf.initBufSize { + // Shrink the buffer back to desired capacity. + s.buf.ResetSize() + } + return nil +} + +func (s *tcpLineSender) AtNow(ctx context.Context) error { + return s.At(ctx, time.Time{}) +} + +func (s *tcpLineSender) At(ctx context.Context, ts time.Time) error { + sendTs := true + if ts.IsZero() { + sendTs = false + } + + err := s.buf.At(ts, sendTs) + if err != nil { + return err + } + + if s.buf.Len() > s.buf.initBufSize { + return s.Flush(ctx) + } + return nil +} + +// Messages returns a copy of accumulated ILP messages that are not +// flushed to the TCP connection yet. Useful for debugging purposes. +func (s *tcpLineSender) Messages() string { + return s.buf.Messages() +} + +// MsgCount returns the number of buffered messages +func (s *tcpLineSender) MsgCount() int { + return s.buf.msgCount +} + +// BufLen returns the number of bytes written to the buffer. +func (s *tcpLineSender) BufLen() int { + return s.buf.Len() +} diff --git a/tcp_sender_test.go b/tcp_sender_test.go new file mode 100644 index 0000000..b745888 --- /dev/null +++ b/tcp_sender_test.go @@ -0,0 +1,266 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "context" + "fmt" + "testing" + "time" + + qdb "github.com/questdb/go-questdb-client/v3" + "github.com/stretchr/testify/assert" +) + +const ( + testTable = "my_test_table" + networkName = "test-network-v3" +) + +type tcpConfigTestCase struct { + name string + config string + expectedErr string +} + +func TestTcpHappyCasesFromConf(t *testing.T) { + var ( + initBufSize = 1000 + ) + + testServer, err := newTestTcpServer(readAndDiscard) + assert.NoError(t, err) + defer testServer.Close() + + addr := testServer.Addr() + + testCases := []tcpConfigTestCase{ + { + name: "addr only", + config: fmt.Sprintf("tcp::addr=%s", addr), + }, + { + name: "init_buf_size", + config: fmt.Sprintf("tcp::addr=%s;init_buf_size=%d", + addr, initBufSize), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sender, err := qdb.LineSenderFromConf(context.Background(), tc.config) + assert.NoError(t, err) + + sender.Close(context.Background()) + }) + } +} + +func TestTcpPathologicalCasesFromConf(t *testing.T) { + testCases := []tcpConfigTestCase{ + { + name: "request_timeout", + config: "tcp::request_timeout=5", + expectedErr: "requestTimeout setting is not available", + }, + { + name: "retry_timeout", + config: "tcp::retry_timeout=5", + expectedErr: "retryTimeout setting is not available", + }, + { + name: "min_throughput", + config: "tcp::min_throughput=5", + expectedErr: "minThroughput setting is not available", + }, + { + name: "auto_flush_rows", + config: "tcp::auto_flush_rows=5", + expectedErr: "autoFlushRows setting is not available", + }, + { + name: "auto_flush_interval", + config: "tcp::auto_flush_interval=5", + expectedErr: "autoFlushInterval setting is not available", + }, + { + name: "tcp key but no id", + config: "tcp::token=test_key", + expectedErr: "tcpKeyId is empty", + }, + { + name: "tcp key id but no key", + config: "tcp::username=test_key_id", + expectedErr: "tcpKey is empty", + }, + { + name: "invalid private key size", + config: "tcp::username=test_key_id;token=1234567890", + expectedErr: "invalid auth key", + }, + { + name: "max_buf_size is set", + config: "tcp::max_buf_size=1000", + expectedErr: "maxBufferSize setting is not available", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := qdb.LineSenderFromConf(context.Background(), tc.config) + assert.ErrorContains(t, err, tc.expectedErr) + }) + } +} +func TestErrorOnFlushWhenMessageIsPending(t *testing.T) { + ctx := context.Background() + + srv, err := newTestTcpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + sender.Table(testTable) + err = sender.Flush(ctx) + + assert.ErrorContains(t, err, "pending ILP message must be finalized with At or AtNow before calling Flush") + assert.Empty(t, qdb.Messages(sender)) +} + +func TestErrorOnUnavailableServer(t *testing.T) { + ctx := context.Background() + + _, err := qdb.NewLineSender(ctx, qdb.WithTcp()) + assert.ErrorContains(t, err, "failed to connect to server") +} + +func TestErrorOnCancelledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + srv, err := newTestTcpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + // The context is not cancelled yet, so Flush should succeed. + err = sender.Table(testTable).StringColumn("foo", "bar").AtNow(ctx) + assert.NoError(t, err) + err = sender.Flush(ctx) + assert.NoError(t, err) + + cancel() + + // The context is now cancelled, so we expect an error. + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + assert.NoError(t, err) + err = sender.Flush(ctx) + assert.Error(t, err) +} + +func TestErrorOnContextDeadline(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(50*time.Millisecond)) + defer cancel() + + srv, err := newTestTcpServer(readAndDiscard) + assert.NoError(t, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(srv.Addr())) + assert.NoError(t, err) + defer sender.Close(ctx) + + // Keep writing until we get an error due to the context deadline. + for i := 0; i < 100_000; i++ { + err = sender.Table(testTable).StringColumn("bar", "baz").AtNow(ctx) + if err != nil { + return + } + err = sender.Flush(ctx) + if err != nil { + return + } + time.Sleep(5 * time.Millisecond) + } + t.Fail() +} + +func BenchmarkLineSenderBatch1000(b *testing.B) { + ctx := context.Background() + + srv, err := newTestTcpServer(readAndDiscard) + assert.NoError(b, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(srv.Addr())) + assert.NoError(b, err) + defer sender.Close(ctx) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 1000; j++ { + sender. + Table(testTable). + Symbol("sym_col", "test_ilp1"). + Float64Column("double_col", float64(i)+0.42). + Int64Column("long_col", int64(i)). + StringColumn("str_col", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"). + BoolColumn("bool_col", true). + TimestampColumn("timestamp_col", time.UnixMicro(42)). + At(ctx, time.UnixMicro(int64(1000*i))) + } + sender.Flush(ctx) + } +} + +func BenchmarkLineSenderNoFlush(b *testing.B) { + ctx := context.Background() + + srv, err := newTestTcpServer(readAndDiscard) + assert.NoError(b, err) + defer srv.Close() + + sender, err := qdb.NewLineSender(ctx, qdb.WithTcp(), qdb.WithAddress(srv.Addr())) + assert.NoError(b, err) + defer sender.Close(ctx) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + sender. + Table(testTable). + Symbol("sym_col", "test_ilp1"). + Float64Column("double_col", float64(i)+0.42). + Int64Column("long_col", int64(i)). + StringColumn("str_col", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"). + BoolColumn("bool_col", true). + TimestampColumn("timestamp_col", time.UnixMicro(42)). + At(ctx, time.UnixMicro(int64(1000*i))) + } + sender.Flush(ctx) +} diff --git a/test/haproxy.cfg b/test/haproxy.cfg index 165ff29..eac3e50 100644 --- a/test/haproxy.cfg +++ b/test/haproxy.cfg @@ -17,3 +17,13 @@ backend ilp mode tcp balance leastconn server questdb questdb:9009 verify none + +userlist httpcreds + user joe insecure-password joespassword + +frontend httpbasicauthfront + bind 0.0.0.0:8445 ssl crt /usr/local/etc/haproxy/haproxy.pem + mode http + http-request auth unless { http_auth(httpcreds) } + default_backend http + diff --git a/test/interop b/test/interop/questdb-client-test similarity index 100% rename from test/interop rename to test/interop/questdb-client-test diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..92845d3 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,253 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2022 QuestDB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +package questdb_test + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "reflect" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type serverType int64 + +const ( + sendToBackChannel serverType = 0 + readAndDiscard serverType = 1 + returning500 serverType = 2 + returning403 serverType = 3 + returning404 serverType = 4 +) + +type testServer struct { + addr string + tcpListener net.Listener + serverType serverType + BackCh chan string + closeCh chan struct{} + wg sync.WaitGroup +} + +func (t *testServer) Addr() string { + return t.addr +} + +func newTestTcpServer(serverType serverType) (*testServer, error) { + return newTestServerWithProtocol(serverType, "tcp") +} + +func newTestHttpServer(serverType serverType) (*testServer, error) { + return newTestServerWithProtocol(serverType, "http") +} + +func newTestServerWithProtocol(serverType serverType, protocol string) (*testServer, error) { + tcp, err := net.Listen("tcp", "127.0.0.1:") + if err != nil { + return nil, err + } + s := &testServer{ + addr: tcp.Addr().String(), + tcpListener: tcp, + serverType: serverType, + BackCh: make(chan string, 5), + closeCh: make(chan struct{}), + } + + switch protocol { + case "tcp": + s.wg.Add(1) + go s.serveTcp() + case "http": + go s.serveHttp() + default: + return nil, fmt.Errorf("invalid protocol %q", protocol) + } + + return s, nil +} + +func (s *testServer) serveTcp() { + defer s.wg.Done() + + for { + conn, err := s.tcpListener.Accept() + if err != nil { + select { + case <-s.closeCh: + return + default: + log.Println("could not accept", err) + } + continue + } + + s.wg.Add(1) + go func() { + switch s.serverType { + case sendToBackChannel: + s.handleSendToBackChannel(conn) + case readAndDiscard: + s.handleReadAndDiscard(conn) + default: + panic(fmt.Sprintf("server type is not supported: %d", s.serverType)) + } + s.wg.Done() + }() + } +} + +func (s *testServer) handleSendToBackChannel(conn net.Conn) { + defer conn.Close() + + r := bufio.NewReader(conn) + for { + select { + case <-s.closeCh: + return + default: + l, err := r.ReadString('\n') + if err != nil { + if err == io.EOF { + continue + } else { + log.Println("could not read", err) + return + } + } + // Remove trailing \n and send line to back channel. + s.BackCh <- l[0 : len(l)-1] + } + } +} + +func (s *testServer) handleReadAndDiscard(conn net.Conn) { + defer conn.Close() + + for { + select { + case <-s.closeCh: + return + default: + _, err := io.Copy(io.Discard, conn) + if err != nil { + if err == io.EOF { + continue + } else { + log.Println("could not read", err) + return + } + } + } + } +} + +func (s *testServer) serveHttp() { + lineFeed := make(chan string) + + go func() { + for { + select { + case <-s.closeCh: + return + case l := <-lineFeed: + s.BackCh <- l + } + } + }() + + http.Serve(s.tcpListener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + err error + ) + + switch s.serverType { + case sendToBackChannel: + r := bufio.NewReader(r.Body) + var l string + for err == nil { + l, err = r.ReadString('\n') + if err == nil && len(l) > 0 { + lineFeed <- l[0 : len(l)-1] + } + } + case readAndDiscard: + _, err = io.Copy(ioutil.Discard, r.Body) + case returning500: + w.WriteHeader(http.StatusInternalServerError) + case returning403: + w.WriteHeader(http.StatusForbidden) + io.WriteString(w, "Forbidden") + case returning404: + w.WriteHeader(http.StatusNotFound) + data, err := json.Marshal(map[string]interface{}{ + "code": "404", + "message": "Not Found", + "line": 42, + "errorId": "Not Found", + }) + if err != nil { + panic(err) + } + w.Write(data) + default: + panic(fmt.Sprintf("server type is not supported: %d", s.serverType)) + } + + if err != nil { + if err != io.EOF { + log.Println("could not read", err) + } + } + })) +} + +func (s *testServer) Close() { + close(s.closeCh) + s.tcpListener.Close() + s.wg.Wait() +} + +func expectLines(t *testing.T, linesCh chan string, expected []string) { + actual := make([]string, 0) + assert.Eventually(t, func() bool { + select { + case l := <-linesCh: + actual = append(actual, l) + default: + return false + } + return reflect.DeepEqual(expected, actual) + }, 3*time.Second, 100*time.Millisecond) +}