New Upstream Release - golang-github-vulcand-oxy
Ready changes
Summary
Merged new upstream version: 1.4.2 (was: 1.3.0).
Resulting package
Built on 2023-01-28T01:42 (took 3m40s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases golang-github-vulcand-oxy-dev
Lintian Result
Diff
diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml
new file mode 100644
index 0000000..8435019
--- /dev/null
+++ b/.github/workflows/go-cross.yml
@@ -0,0 +1,55 @@
+name: Go Matrix
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+jobs:
+
+ cross:
+ name: Go
+ runs-on: ${{ matrix.os }}
+ env:
+ CGO_ENABLED: 0
+
+ strategy:
+ matrix:
+ go-version: [ 1.17, 1.18, 1.x ]
+ os: [ubuntu-latest, macos-latest]
+ # TODO ignore windows but need to be added in the future
+ # os: [ubuntu-latest, macos-latest, windows-latest]
+
+ steps:
+ # https://github.com/marketplace/actions/setup-go-environment
+ - name: Set up Go ${{ matrix.go-version }}
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go-version }}
+
+ # https://github.com/marketplace/actions/checkout
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ # https://github.com/marketplace/actions/cache
+ - name: Cache Go modules
+ uses: actions/cache@v2
+ with:
+ # In order:
+ # * Module download cache
+ # * Build cache (Linux)
+ # * Build cache (Mac)
+ # * Build cache (Windows)
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ ~/Library/Caches/go-build
+ %LocalAppData%\go-build
+ key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ matrix.go-version }}-go-
+
+ - name: Test
+ run: go test -v -cover ./...
+
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
new file mode 100644
index 0000000..91868ad
--- /dev/null
+++ b/.github/workflows/pr.yml
@@ -0,0 +1,52 @@
+name: Main
+
+on:
+ pull_request:
+
+jobs:
+
+ main:
+ name: Main Process
+ runs-on: ubuntu-latest
+ env:
+ GO_VERSION: 1.17
+ GOLANGCI_LINT_VERSION: v1.45.2
+
+ steps:
+
+ # https://github.com/marketplace/actions/setup-go-environment
+ - name: Set up Go ${{ env.GO_VERSION }}
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ # https://github.com/marketplace/actions/checkout
+ - name: Check out code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ # https://github.com/marketplace/actions/cache
+ - name: Cache Go modules
+ uses: actions/cache@v2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Check and get dependencies
+ run: |
+ go mod tidy
+ git diff --exit-code go.mod
+ git diff --exit-code go.sum
+
+ # https://golangci-lint.run/usage/install#other-ci
+ - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
+ run: |
+ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
+ golangci-lint --version
+
+ - name: Make
+ run: make
+
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..7d43893
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,146 @@
+run:
+ deadline: 5m
+ skip-files: [ ]
+ skip-dirs: ["internal/holsterv4"]
+
+linters-settings:
+ govet:
+ enable-all: true
+ disable:
+ - fieldalignment
+ - shadow
+ gocyclo:
+ min-complexity: 15
+ maligned:
+ suggest-new: true
+ goconst:
+ min-len: 5
+ min-occurrences: 3
+ misspell:
+ locale: US
+ funlen:
+ lines: -1
+ statements: 50
+ godox:
+ keywords:
+ - FIXME
+ gofumpt:
+ extra-rules: false
+ depguard:
+ list-type: blacklist
+ include-go-root: false
+ packages:
+ - github.com/pkg/errors
+ gocritic:
+ enabled-tags:
+ - diagnostic
+ - style
+ - performance
+ disabled-checks:
+ - sloppyReassign
+ - rangeValCopy
+ - octalLiteral
+ - paramTypeCombine # already handle by gofumpt.extra-rules
+ - httpNoBody
+ - unnamedResult
+ - deferInLoop # TODO(ldez) should be use on the project
+ settings:
+ hugeParam:
+ sizeThreshold: 100
+
+linters:
+ enable-all: true
+ disable:
+ - maligned # deprecated
+ - interfacer # deprecated
+ - scopelint # deprecated
+ - golint # deprecated
+ - sqlclosecheck # not relevant (SQL)
+ - rowserrcheck # not relevant (SQL)
+ - cyclop # duplicate of gocyclo
+ - lll
+ - dupl
+ - wsl
+ - nlreturn
+ - gomnd
+ - goerr113
+ - wrapcheck
+ - exhaustive
+ - exhaustivestruct
+ - testpackage
+ - tparallel
+ - paralleltest
+ - prealloc
+ - ifshort
+ - forcetypeassert
+ - bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
+ - varnamelen
+ - noctx
+ - tagliatelle
+ - nilnil
+ - ireturn
+ - gochecknoglobals # TODO(ldez) should be use on the project
+ - errorlint # TODO(ldez) should be use on the project
+ - nestif # TODO(ldez) should be use on the project
+
+issues:
+ exclude-use-default: false
+ max-per-linter: 0
+ max-same-issues: 0
+ exclude:
+ - 'Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked'
+ - 'SA1019: http.CloseNotifier has been deprecated'
+ - 'string `https` has 3 occurrences, make it a constant'
+
+ - 'ST1003: method ToJson should be ToJSON' # TODO(ldez) must be fixed
+ - 'ST1003: type SerializableHttpRequest should be SerializableHTTPRequest' # TODO(ldez) must be fixed
+ - 'ST1003: func DumpHttpRequest should be DumpHTTPRequest' # TODO(ldez) must be fixed
+ - 'ST1003: const XRealIp should be XRealIP' # TODO(ldez) must be fixed
+ - 'ST1003: type UrlForwardingStateListener should be URLForwardingStateListener' # TODO(ldez) must be fixed
+ - 'var-naming: type UrlForwardingStateListener should be URLForwardingStateListener' # TODO(ldez) must be fixed
+ - 'var-naming: const XRealIp should be XRealIP' # TODO(ldez) must be fixed
+ - 'var-naming: method ToJson should be ToJSON' # TODO(ldez) must be fixed
+ - 'var-naming: type SerializableHttpRequest should be SerializableHTTPRequest' # TODO(ldez) must be fixed
+ - 'var-naming: func DumpHttpRequest should be DumpHTTPRequest' # TODO(ldez) must be fixed
+
+ - 'exported: func name will be used as roundrobin.RoundRobinLogger by other packages'# TODO(ldez) must be fixed
+ - 'exported: func name will be used as roundrobin.RoundRobinRequestRewriteListener by other packages'# TODO(ldez) must be fixed
+ - 'exported: type name will be used as connlimit.ConnLimitOption by other packages'# TODO(ldez) must be fixed
+
+ - 'ST1000: at least one file in a package should have a package comment' # TODO(ldez) must be fixed
+ - 'SA1019: tls.VersionSSL30 has been deprecated' # TODO(ldez) must be fixed
+ - 'Error return value of `resp.Body.Close` is not checked' # TODO(ldez) must be fixed
+ - '`marshalling` is a misspelling of `marshaling`' # TODO(ldez) must be fixed
+
+ - 'ST1005: error strings should not be capitalized'# TODO(ldez) must be fixed
+ - 'ST1005: error strings should not end with punctuation or a newline' # TODO(ldez) must be fixed
+ - 'error-strings: error strings should not be capitalized or end with punctuation or a newline' # TODO(ldez) must be fixed
+
+ - 'unexported-return: exported func ([^ ]+) returns unexported type stream.optSetter, which can be annoying to use' # TODO(ldez) must be fixed
+ - 'unexported-return: exported func ([^ ]+) returns unexported type buffer.optSetter, which can be annoying to use' # TODO(ldez) must be fixed
+ - 'unexported-return: exported func ([^ ]+) returns unexported type forward.optSetter, which can be annoying to use' # TODO(ldez) must be fixed
+ - 'unexported-return: exported func ([^ ]+) returns unexported type memmetrics.rrOptSetter, which can be annoying to use' # TODO(ldez) must be fixed
+ - 'unexported-return: exported func ([^ ]+) returns unexported type memmetrics.rcOptSetter, which can be annoying to use' # TODO(ldez) must be fixed
+ - 'unexported-return: exported func ([^ ]+) returns unexported type memmetrics.rhOptSetter, which can be annoying to use' # TODO(ldez) must be fixed
+ - 'unexported-return: exported func ([^ ]+) returns unexported type memmetrics.ratioOptSetter, which can be annoying to use' # TODO(ldez) must be fixed
+
+ exclude-rules:
+ - path: .*_test.go
+ linters:
+ - funlen
+ - gosec
+ - path: testutils/.+
+ linters:
+ - gosec
+ - path: cbreaker/cbreaker_test.go
+ text: "`statsNetErrors` - `threshold` always receives `0.6`" # TODO(ldez) must be fixed
+ - path: buffer/buffer.go
+ text: "(cognitive|cyclomatic) complexity \\d+ of func `\\(\\*Buffer\\)\\.ServeHTTP` is high" # TODO(ldez) must be fixed
+ - path: buffer/buffer.go
+ text: "Function 'ServeHTTP' has too many statements" # TODO(ldez) must be fixed
+ - path: forward/fwd.go
+ text: "(cognitive|cyclomatic) complexity \\d+ of func `\\(\\*httpForwarder\\)\\.serveWebSocket` is high" # TODO(ldez) must be fixed
+ - path: forward/fwd.go
+ text: "Function 'serveWebSocket' has too many statements" # TODO(ldez) must be fixed
+ - path: utils/handler.go
+ text: "ifElseChain: rewrite if-else to switch statement" # TODO(ldez) must be fixed
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 0028b69..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-language: go
-
-go:
- - 1.15.x
- - 1.x
-
-go_import_path: github.com/vulcand/oxy
-
-notifications:
- email:
- on_success: never
- on_failure: change
-
-env:
- - GO111MODULE=on
-
-before_install:
- - GO111MODULE=off go get -u golang.org/x/lint/golint
- - GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell
-
-install:
- - go mod tidy
- - git diff --exit-code go.mod go.sum
diff --git a/Makefile b/Makefile
index b313147..fb79148 100644
--- a/Makefile
+++ b/Makefile
@@ -1,50 +1,18 @@
-.PHONY: all
+.PHONY: default clean checks test test-verbose
export GO111MODULE=on
-PKGS := $(shell go list ./... | grep -v '/vendor/')
-GOFILES := $(shell go list -f '{{range $$index, $$element := .GoFiles}}{{$$.Dir}}/{{$$element}}{{"\n"}}{{end}}' ./... | grep -v '/vendor/')
-TXT_FILES := $(shell find * -type f -not -path 'vendor/**')
-
-default: clean misspell vet check-fmt test
+default: clean checks test
test: clean
- go test -race -cover $(PKGS)
+ go test -race -cover -count 1 ./...
test-verbose: clean
- go test -v -race -cover $(PKGS)
+ go test -v -race -cover ./...
clean:
find . -name flymake_* -delete
rm -f cover.out
-lint:
- echo "golint:"
- golint -set_exit_status $(PKGS)
-
-vet:
- go vet $(PKGS)
-
-checks: vet lint check-fmt
- staticcheck $(PKGS)
- gosimple $(PKGS)
-
-check-fmt: SHELL := /bin/bash
-check-fmt:
- diff -u <(echo -n) <(gofmt -d $(GOFILES))
-
-misspell:
- misspell -source=text -error $(TXT_FILES)
-
-test-package: clean
- go test -v ./$(p)
-
-test-grep-package: clean
- go test -v ./$(p) -check.f=$(e)
-
-cover-package: clean
- go test -v ./$(p) -coverprofile=/tmp/coverage.out
- go tool cover -html=/tmp/coverage.out
-
-sloccount:
- find . -path ./vendor -prune -o -name "*.go" -print0 | xargs -0 wc -l
+checks:
+ golangci-lint run
diff --git a/buffer/buffer.go b/buffer/buffer.go
index 00dfa9c..89b9104 100644
--- a/buffer/buffer.go
+++ b/buffer/buffer.go
@@ -39,7 +39,6 @@ import (
"bufio"
"fmt"
"io"
- "io/ioutil"
"net"
"net/http"
"reflect"
@@ -50,57 +49,17 @@ import (
)
const (
- // DefaultMemBodyBytes Store up to 1MB in RAM
+ // DefaultMemBodyBytes Store up to 1MB in RAM.
DefaultMemBodyBytes = 1048576
- // DefaultMaxBodyBytes No limit by default
+ // DefaultMaxBodyBytes No limit by default.
DefaultMaxBodyBytes = -1
- // DefaultMaxRetryAttempts Maximum retry attempts
+ // DefaultMaxRetryAttempts Maximum retry attempts.
DefaultMaxRetryAttempts = 10
)
var errHandler utils.ErrorHandler = &SizeErrHandler{}
-// Buffer is responsible for buffering requests and responses
-// It buffers large requests and responses to disk,
-type Buffer struct {
- maxRequestBodyBytes int64
- memRequestBodyBytes int64
-
- maxResponseBodyBytes int64
- memResponseBodyBytes int64
-
- retryPredicate hpredicate
-
- next http.Handler
- errHandler utils.ErrorHandler
-
- log *log.Logger
-}
-
-// New returns a new buffer middleware. New() function supports optional functional arguments
-func New(next http.Handler, setters ...optSetter) (*Buffer, error) {
- strm := &Buffer{
- next: next,
-
- maxRequestBodyBytes: DefaultMaxBodyBytes,
- memRequestBodyBytes: DefaultMemBodyBytes,
-
- maxResponseBodyBytes: DefaultMaxBodyBytes,
- memResponseBodyBytes: DefaultMemBodyBytes,
-
- log: log.StandardLogger(),
- }
- for _, s := range setters {
- if err := s(strm); err != nil {
- return nil, err
- }
- }
- if strm.errHandler == nil {
- strm.errHandler = errHandler
- }
-
- return strm, nil
-}
+type optSetter func(b *Buffer) error
// Logger defines the logger the buffer will use.
//
@@ -112,8 +71,6 @@ func Logger(l *log.Logger) optSetter {
}
}
-type optSetter func(b *Buffer) error
-
// CondSetter Conditional setter.
// ex: Cond(a > 4, MemRequestBodyBytes(a))
func CondSetter(condition bool, setter optSetter) optSetter {
@@ -135,7 +92,7 @@ func CondSetter(condition bool, setter optSetter) optSetter {
//
// Example of the predicate:
//
-// `Attempts() <= 2 && ResponseCode() == 502`
+// `Attempts() <= 2 && ResponseCode() == 502`.
//
func Retry(predicate string) optSetter {
return func(b *Buffer) error {
@@ -148,7 +105,7 @@ func Retry(predicate string) optSetter {
}
}
-// ErrorHandler sets error handler of the server
+// ErrorHandler sets error handler of the server.
func ErrorHandler(h utils.ErrorHandler) optSetter {
return func(b *Buffer) error {
b.errHandler = h
@@ -156,7 +113,7 @@ func ErrorHandler(h utils.ErrorHandler) optSetter {
}
}
-// MaxRequestBodyBytes sets the maximum request body size in bytes
+// MaxRequestBodyBytes sets the maximum request body size in bytes.
func MaxRequestBodyBytes(m int64) optSetter {
return func(b *Buffer) error {
if m < 0 {
@@ -179,7 +136,7 @@ func MemRequestBodyBytes(m int64) optSetter {
}
}
-// MaxResponseBodyBytes sets the maximum response body size in bytes
+// MaxResponseBodyBytes sets the maximum response body size in bytes.
func MaxResponseBodyBytes(m int64) optSetter {
return func(b *Buffer) error {
if m < 0 {
@@ -202,6 +159,48 @@ func MemResponseBodyBytes(m int64) optSetter {
}
}
+// Buffer is responsible for buffering requests and responses
+// It buffers large requests and responses to disk,.
+type Buffer struct {
+ maxRequestBodyBytes int64
+ memRequestBodyBytes int64
+
+ maxResponseBodyBytes int64
+ memResponseBodyBytes int64
+
+ retryPredicate hpredicate
+
+ next http.Handler
+ errHandler utils.ErrorHandler
+
+ log *log.Logger
+}
+
+// New returns a new buffer middleware. New() function supports optional functional arguments.
+func New(next http.Handler, setters ...optSetter) (*Buffer, error) {
+ strm := &Buffer{
+ next: next,
+
+ maxRequestBodyBytes: DefaultMaxBodyBytes,
+ memRequestBodyBytes: DefaultMemBodyBytes,
+
+ maxResponseBodyBytes: DefaultMaxBodyBytes,
+ memResponseBodyBytes: DefaultMemBodyBytes,
+
+ log: log.StandardLogger(),
+ }
+ for _, s := range setters {
+ if err := s(strm); err != nil {
+ return nil, err
+ }
+ }
+ if strm.errHandler == nil {
+ strm.errHandler = errHandler
+ }
+
+ return strm, nil
+}
+
// Wrap sets the next handler to be called by buffer handler.
func (b *Buffer) Wrap(next http.Handler) error {
b.next = next
@@ -210,7 +209,7 @@ func (b *Buffer) Wrap(next http.Handler) error {
func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if b.log.Level >= log.DebugLevel {
- logEntry := b.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := b.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/buffer: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/buffer: completed ServeHttp on request")
}
@@ -301,7 +300,7 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
utils.CopyHeaders(w.Header(), bw.Header())
w.WriteHeader(bw.code)
if reader != nil {
- io.Copy(w, reader)
+ _, _ = io.Copy(w, reader)
}
return
}
@@ -330,9 +329,9 @@ func (b *Buffer) copyRequest(req *http.Request, body io.ReadCloser, bodySize int
o.TransferEncoding = []string{}
// http.Transport will close the request body on any error, we are controlling the close process ourselves, so we override the closer here
if body == nil {
- o.Body = ioutil.NopCloser(req.Body)
+ o.Body = io.NopCloser(req.Body)
} else {
- o.Body = ioutil.NopCloser(body.(io.Reader))
+ o.Body = io.NopCloser(body.(io.Reader))
}
return &o
}
@@ -356,7 +355,7 @@ type bufferWriter struct {
log *log.Logger
}
-// RFC2616 #4.4
+// RFC2616 #4.4.
func (b *bufferWriter) expectBody(r *http.Request) bool {
if r.Method == "HEAD" {
return false
@@ -398,7 +397,7 @@ func (b *bufferWriter) WriteHeader(code int) {
b.code = code
}
-// CloseNotifier interface - this allows downstream connections to be terminated when the client terminates.
+// CloseNotify CloseNotifier interface - this allows downstream connections to be terminated when the client terminates.
func (b *bufferWriter) CloseNotify() <-chan bool {
if cn, ok := b.responseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify()
@@ -411,22 +410,22 @@ func (b *bufferWriter) CloseNotify() <-chan bool {
func (b *bufferWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hi, ok := b.responseWriter.(http.Hijacker); ok {
conn, rw, err := hi.Hijack()
- if err != nil {
+ if err == nil {
b.hijacked = true
}
return conn, rw, err
}
- b.log.Warningf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(b.responseWriter))
+ b.log.Warningf("Upstream ResponseWriter of type %v does not implement http.Hijacker.", reflect.TypeOf(b.responseWriter))
return nil, nil, fmt.Errorf("the response writer wrapped in this proxy does not implement http.Hijacker. Its type is: %v", reflect.TypeOf(b.responseWriter))
}
-// SizeErrHandler Size error handler
+// SizeErrHandler Size error handler.
type SizeErrHandler struct{}
func (e *SizeErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) {
if _, ok := err.(*multibuf.MaxSizeReachedError); ok {
w.WriteHeader(http.StatusRequestEntityTooLarge)
- w.Write([]byte(http.StatusText(http.StatusRequestEntityTooLarge)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusRequestEntityTooLarge)))
return
}
utils.DefaultHandler.ServeHTTP(w, req, err)
diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go
index 0b27554..ec441c9 100644
--- a/buffer/buffer_test.go
+++ b/buffer/buffer_test.go
@@ -4,7 +4,7 @@ import (
"bufio"
"crypto/tls"
"fmt"
- "io/ioutil"
+ "io"
"net"
"net/http"
"net/http/httptest"
@@ -20,7 +20,7 @@ import (
func TestSimple(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -51,11 +51,11 @@ func TestChunkedEncodingSuccess(t *testing.T) {
var reqBody string
var contentLength int64
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- body, err := ioutil.ReadAll(req.Body)
+ body, err := io.ReadAll(req.Body)
require.NoError(t, err)
reqBody = string(body)
contentLength = req.ContentLength
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -79,7 +79,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
conn, err := net.Dial("tcp", testutils.ParseURI(proxy.URL).Host)
require.NoError(t, err)
- fmt.Fprintf(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
+ _, _ = fmt.Fprintf(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
require.NoError(t, err)
@@ -90,7 +90,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
func TestChunkedEncodingLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -113,7 +113,7 @@ func TestChunkedEncodingLimitReached(t *testing.T) {
conn, err := net.Dial("tcp", testutils.ParseURI(proxy.URL).Host)
require.NoError(t, err)
- fmt.Fprint(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
+ _, _ = fmt.Fprint(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
require.NoError(t, err)
@@ -124,8 +124,8 @@ func TestChunkedResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
h := w.(http.Hijacker)
conn, _, _ := h.Hijack()
- fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
- conn.Close()
+ _, _ = fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
+ _ = conn.Close()
})
defer srv.Close()
@@ -151,7 +151,7 @@ func TestChunkedResponse(t *testing.T) {
func TestRequestLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -179,7 +179,7 @@ func TestRequestLimitReached(t *testing.T) {
func TestResponseLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello, this response is too large"))
+ _, _ = w.Write([]byte("hello, this response is too large"))
})
defer srv.Close()
@@ -207,7 +207,7 @@ func TestResponseLimitReached(t *testing.T) {
func TestFileStreamingResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello, this response is too large to fit in memory"))
+ _, _ = w.Write([]byte("hello, this response is too large to fit in memory"))
})
defer srv.Close()
@@ -236,7 +236,7 @@ func TestFileStreamingResponse(t *testing.T) {
func TestCustomErrorHandler(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello, this response is too large"))
+ _, _ = w.Write([]byte("hello, this response is too large"))
})
defer srv.Close()
@@ -253,7 +253,7 @@ func TestCustomErrorHandler(t *testing.T) {
// stream handler will forward requests to redirect
errHandler := utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
- w.Write([]byte(http.StatusText(http.StatusTeapot)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})
st, err := New(rdr, MaxResponseBodyBytes(4), ErrorHandler(errHandler))
require.NoError(t, err)
@@ -322,11 +322,11 @@ func TestNoBody(t *testing.T) {
assert.Equal(t, http.StatusOK, re.StatusCode)
}
-// Make sure that stream handler preserves TLS settings
+// Make sure that stream handler preserves TLS settings.
func TestPreservesTLS(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
- w.Write([]byte("ok"))
+ _, _ = w.Write([]byte("ok"))
})
defer srv.Close()
@@ -358,7 +358,7 @@ func TestPreservesTLS(t *testing.T) {
func TestNotNilBody(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
diff --git a/buffer/retry_test.go b/buffer/retry_test.go
index bb2880e..ac8b7c9 100644
--- a/buffer/retry_test.go
+++ b/buffer/retry_test.go
@@ -14,7 +14,7 @@ import (
func TestSuccess(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -33,7 +33,7 @@ func TestSuccess(t *testing.T) {
func TestRetryOnError(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -53,7 +53,7 @@ func TestRetryOnError(t *testing.T) {
func TestRetryExceedAttempts(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -73,6 +73,8 @@ func TestRetryExceedAttempts(t *testing.T) {
}
func newBufferMiddleware(t *testing.T, p string) (*roundrobin.RoundRobin, *Buffer) {
+ t.Helper()
+
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
diff --git a/buffer/threshold.go b/buffer/threshold.go
index 0fdde7d..1bb712e 100644
--- a/buffer/threshold.go
+++ b/buffer/threshold.go
@@ -7,7 +7,7 @@ import (
"github.com/vulcand/predicate"
)
-// IsValidExpression check if it's a valid expression
+// IsValidExpression check if it's a valid expression.
func IsValidExpression(expr string) bool {
_, err := parseExpression(expr)
return err == nil
@@ -21,7 +21,7 @@ type context struct {
type hpredicate func(*context) bool
-// Parses expression in the go language into Failover predicates
+// Parses expression in the go language into Failover predicates.
func parseExpression(in string) (hpredicate, error) {
p, err := predicate.NewParser(predicate.Def{
Operators: predicate.Operators{
@@ -56,16 +56,17 @@ func parseExpression(in string) (hpredicate, error) {
}
type toString func(c *context) string
+
type toInt func(c *context) int
-// RequestMethod returns mapper of the request to its method e.g. POST
+// RequestMethod returns mapper of the request to its method e.g. POST.
func requestMethod() toString {
return func(c *context) string {
return c.r.Method
}
}
-// Attempts returns mapper of the request to the number of proxy attempts
+// Attempts returns mapper of the request to the number of proxy attempts.
func attempts() toInt {
return func(c *context) int {
return c.attempt
@@ -86,7 +87,7 @@ func isNetworkError() hpredicate {
}
}
-// and returns predicate by joining the passed predicates with logical 'and'
+// and returns predicate by joining the passed predicates with logical 'and'.
func and(fns ...hpredicate) hpredicate {
return func(c *context) bool {
for _, fn := range fns {
@@ -98,7 +99,7 @@ func and(fns ...hpredicate) hpredicate {
}
}
-// or returns predicate by joining the passed predicates with logical 'or'
+// or returns predicate by joining the passed predicates with logical 'or'.
func or(fns ...hpredicate) hpredicate {
return func(c *context) bool {
for _, fn := range fns {
@@ -110,14 +111,14 @@ func or(fns ...hpredicate) hpredicate {
}
}
-// not creates negation of the passed predicate
+// not creates negation of the passed predicate.
func not(p hpredicate) hpredicate {
return func(c *context) bool {
return !p(c)
}
}
-// eq returns predicate that tests for equality of the value of the mapper and the constant
+// eq returns predicate that tests for equality of the value of the mapper and the constant.
func eq(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toString:
@@ -128,7 +129,7 @@ func eq(m interface{}, value interface{}) (hpredicate, error) {
return nil, fmt.Errorf("unsupported argument: %T", m)
}
-// neq returns predicate that tests for inequality of the value of the mapper and the constant
+// neq returns predicate that tests for inequality of the value of the mapper and the constant.
func neq(m interface{}, value interface{}) (hpredicate, error) {
p, err := eq(m, value)
if err != nil {
@@ -137,16 +138,17 @@ func neq(m interface{}, value interface{}) (hpredicate, error) {
return not(p), nil
}
-// lt returns predicate that tests that value of the mapper function is less than the constant
+// lt returns predicate that tests that value of the mapper function is less than the constant.
func lt(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
return intLT(mapper, value)
+ default:
+ return nil, fmt.Errorf("unsupported argument: %T", m)
}
- return nil, fmt.Errorf("unsupported argument: %T", m)
}
-// le returns predicate that tests that value of the mapper function is less or equal than the constant
+// le returns predicate that tests that value of the mapper function is less or equal than the constant.
func le(m interface{}, value interface{}) (hpredicate, error) {
l, err := lt(m, value)
if err != nil {
@@ -161,16 +163,17 @@ func le(m interface{}, value interface{}) (hpredicate, error) {
}, nil
}
-// gt returns predicate that tests that value of the mapper function is greater than the constant
+// gt returns predicate that tests that value of the mapper function is greater than the constant.
func gt(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
return intGT(mapper, value)
+ default:
+ return nil, fmt.Errorf("unsupported argument: %T", m)
}
- return nil, fmt.Errorf("unsupported argument: %T", m)
}
-// ge returns predicate that tests that value of the mapper function is less or equal than the constant
+// ge returns predicate that tests that value of the mapper function is less or equal than the constant.
func ge(m interface{}, value interface{}) (hpredicate, error) {
g, err := gt(m, value)
if err != nil {
diff --git a/cbreaker/cbreaker.go b/cbreaker/cbreaker.go
index 86f6ffc..d548988 100644
--- a/cbreaker/cbreaker.go
+++ b/cbreaker/cbreaker.go
@@ -31,13 +31,13 @@ import (
"sync"
"time"
- "github.com/mailgun/timetools"
log "github.com/sirupsen/logrus"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/memmetrics"
"github.com/vulcand/oxy/utils"
)
-// CircuitBreaker is http.Handler that implements circuit breaker pattern
+// CircuitBreaker is http.Handler that implements circuit breaker pattern.
type CircuitBreaker struct {
m *sync.RWMutex
metrics *memmetrics.RTMetrics
@@ -51,28 +51,25 @@ type CircuitBreaker struct {
onStandby SideEffect
state cbState
- until time.Time
+ until clock.Time
rc *ratioController
checkPeriod time.Duration
- lastCheck time.Time
+ lastCheck clock.Time
fallback http.Handler
next http.Handler
- clock timetools.TimeProvider
-
log *log.Logger
}
-// New creates a new CircuitBreaker middleware
+// New creates a new CircuitBreaker middleware.
func New(next http.Handler, expression string, options ...CircuitBreakerOption) (*CircuitBreaker, error) {
cb := &CircuitBreaker{
m: &sync.RWMutex{},
next: next,
// Default values. Might be overwritten by options below.
- clock: &timetools.RealTime{},
checkPeriod: defaultCheckPeriod,
fallbackDuration: defaultFallbackDuration,
recoveryDuration: defaultRecoveryDuration,
@@ -113,7 +110,7 @@ func Logger(l *log.Logger) CircuitBreakerOption {
func (c *CircuitBreaker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if c.log.Level >= log.DebugLevel {
- logEntry := c.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := c.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/circuitbreaker: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/circuitbreaker: completed ServeHttp on request")
}
@@ -134,8 +131,8 @@ func (c *CircuitBreaker) Wrap(next http.Handler) {
c.next = next
}
-// updateState updates internal state and returns true if fallback should be used and false otherwise
-func (c *CircuitBreaker) activateFallback(w http.ResponseWriter, req *http.Request) bool {
+// updateState updates internal state and returns true if fallback should be used and false otherwise.
+func (c *CircuitBreaker) activateFallback(_ http.ResponseWriter, _ *http.Request) bool {
// Quick check with read locks optimized for normal operation use-case
if c.isStandby() {
return false
@@ -151,7 +148,7 @@ func (c *CircuitBreaker) activateFallback(w http.ResponseWriter, req *http.Reque
// someone else has set it to standby just now
return false
case stateTripped:
- if c.clock.UtcNow().Before(c.until) {
+ if clock.Now().UTC().Before(c.until) {
return true
}
// We have been in active state enough, enter recovering state
@@ -159,8 +156,8 @@ func (c *CircuitBreaker) activateFallback(w http.ResponseWriter, req *http.Reque
fallthrough
case stateRecovering:
// We have been in recovering state enough, enter standby and allow request
- if c.clock.UtcNow().After(c.until) {
- c.setState(stateStandby, c.clock.UtcNow())
+ if clock.Now().UTC().After(c.until) {
+ c.setState(stateStandby, clock.Now().UTC())
return false
}
// ratio controller allows this request
@@ -173,12 +170,12 @@ func (c *CircuitBreaker) activateFallback(w http.ResponseWriter, req *http.Reque
}
func (c *CircuitBreaker) serve(w http.ResponseWriter, req *http.Request) {
- start := c.clock.UtcNow()
+ start := clock.Now().UTC()
p := utils.NewProxyWriterWithLogger(w, c.log)
c.next.ServeHTTP(p, req)
- latency := c.clock.UtcNow().Sub(start)
+ latency := clock.Now().UTC().Sub(start)
c.metrics.Record(p.StatusCode(), latency)
// Note that this call is less expensive than it looks -- checkCondition only performs the real check
@@ -192,7 +189,7 @@ func (c *CircuitBreaker) isStandby() bool {
return c.state == stateStandby
}
-// String returns log-friendly representation of the circuit breaker state
+// String returns log-friendly representation of the circuit breaker state.
func (c *CircuitBreaker) String() string {
switch c.state {
case stateTripped, stateRecovering:
@@ -202,7 +199,7 @@ func (c *CircuitBreaker) String() string {
}
}
-// exec executes side effect
+// exec executes side effect.
func (c *CircuitBreaker) exec(s SideEffect) {
if s == nil {
return
@@ -214,11 +211,11 @@ func (c *CircuitBreaker) exec(s SideEffect) {
}()
}
-func (c *CircuitBreaker) setState(new cbState, until time.Time) {
- c.log.Debugf("%v setting state to %v, until %v", c, new, until)
- c.state = new
+func (c *CircuitBreaker) setState(state cbState, until time.Time) {
+ c.log.Debugf("%v setting state to %v, until %v", c, state, until)
+ c.state = state
c.until = until
- switch new {
+ switch state {
case stateTripped:
c.exec(c.onTripped)
case stateStandby:
@@ -229,10 +226,10 @@ func (c *CircuitBreaker) setState(new cbState, until time.Time) {
func (c *CircuitBreaker) timeToCheck() bool {
c.m.RLock()
defer c.m.RUnlock()
- return c.clock.UtcNow().After(c.lastCheck)
+ return clock.Now().UTC().After(c.lastCheck)
}
-// Checks if tripping condition matches and sets circuit breaker to the tripped state
+// Checks if tripping condition matches and sets circuit breaker to the tripped state.
func (c *CircuitBreaker) checkAndSet() {
if !c.timeToCheck() {
return
@@ -242,10 +239,10 @@ func (c *CircuitBreaker) checkAndSet() {
defer c.m.Unlock()
// Other goroutine could have updated the lastCheck variable before we grabbed mutex
- if !c.clock.UtcNow().After(c.lastCheck) {
+ if !clock.Now().UTC().After(c.lastCheck) {
return
}
- c.lastCheck = c.clock.UtcNow().Add(c.checkPeriod)
+ c.lastCheck = clock.Now().UTC().Add(c.checkPeriod)
if c.state == stateTripped {
c.log.Debugf("%v skip set tripped", c)
@@ -256,28 +253,19 @@ func (c *CircuitBreaker) checkAndSet() {
return
}
- c.setState(stateTripped, c.clock.UtcNow().Add(c.fallbackDuration))
+ c.setState(stateTripped, clock.Now().UTC().Add(c.fallbackDuration))
c.metrics.Reset()
}
func (c *CircuitBreaker) setRecovering() {
- c.setState(stateRecovering, c.clock.UtcNow().Add(c.recoveryDuration))
- c.rc = newRatioController(c.clock, c.recoveryDuration, c.log)
+ c.setState(stateRecovering, clock.Now().UTC().Add(c.recoveryDuration))
+ c.rc = newRatioController(c.recoveryDuration, c.log)
}
// CircuitBreakerOption represents an option you can pass to New.
// See the documentation for the individual options below.
type CircuitBreakerOption func(*CircuitBreaker) error
-// Clock allows you to fake che CircuitBreaker's view of the current time.
-// Intended for unit tests.
-func Clock(clock timetools.TimeProvider) CircuitBreakerOption {
- return func(c *CircuitBreaker) error {
- c.clock = clock
- return nil
- }
-}
-
// FallbackDuration is how long the CircuitBreaker will remain in the Tripped
// state before trying to recover.
func FallbackDuration(d time.Duration) CircuitBreakerOption {
@@ -332,7 +320,7 @@ func Fallback(h http.Handler) CircuitBreakerOption {
}
}
-// cbState is the state of the circuit breaker
+// cbState is the state of the circuit breaker.
type cbState int
func (s cbState) String() string {
@@ -348,25 +336,25 @@ func (s cbState) String() string {
}
const (
- // CircuitBreaker is passing all requests and watching stats
+ // CircuitBreaker is passing all requests and watching stats.
stateStandby = iota
- // CircuitBreaker activates fallback scenario for all requests
+ // CircuitBreaker activates fallback scenario for all requests.
stateTripped
- // CircuitBreaker passes some requests to go through, rejecting others
+ // CircuitBreaker passes some requests to go through, rejecting others.
stateRecovering
)
const (
- defaultFallbackDuration = 10 * time.Second
- defaultRecoveryDuration = 10 * time.Second
- defaultCheckPeriod = 100 * time.Millisecond
+ defaultFallbackDuration = 10 * clock.Second
+ defaultRecoveryDuration = 10 * clock.Second
+ defaultCheckPeriod = 100 * clock.Millisecond
)
var defaultFallback = &fallback{}
type fallback struct{}
-func (f *fallback) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+func (f *fallback) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
- w.Write([]byte(http.StatusText(http.StatusServiceUnavailable)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusServiceUnavailable)))
}
diff --git a/cbreaker/cbreaker_test.go b/cbreaker/cbreaker_test.go
index 0129c45..a500de0 100644
--- a/cbreaker/cbreaker_test.go
+++ b/cbreaker/cbreaker_test.go
@@ -2,7 +2,7 @@ package cbreaker
import (
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/memmetrics"
"github.com/vulcand/oxy/testutils"
)
@@ -19,7 +20,7 @@ const triggerNetRatio = `NetworkErrorRatio() > 0.5`
func TestStandbyCycle(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
cb, err := New(handler, triggerNetRatio)
@@ -36,12 +37,13 @@ func TestStandbyCycle(t *testing.T) {
func TestFullCycle(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- cb, err := New(handler, triggerNetRatio, Clock(clock))
+ cb, err := New(handler, triggerNetRatio)
require.NoError(t, err)
srv := httptest.NewServer(cb)
@@ -52,27 +54,27 @@ func TestFullCycle(t *testing.T) {
assert.Equal(t, http.StatusOK, re.StatusCode)
cb.metrics = statsNetErrors(0.6)
- clock.CurrentTime = clock.CurrentTime.Add(defaultCheckPeriod + time.Millisecond)
+ clock.Advance(defaultCheckPeriod + clock.Millisecond)
_, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, cbState(stateTripped), cb.state)
// Some time has passed, but we are still in trapped state.
- clock.CurrentTime = clock.CurrentTime.Add(9 * time.Second)
+ clock.Advance(9 * clock.Second)
re, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode)
assert.Equal(t, cbState(stateTripped), cb.state)
// We should be in recovering state by now
- clock.CurrentTime = clock.CurrentTime.Add(time.Second*1 + time.Millisecond)
+ clock.Advance(clock.Second*1 + clock.Millisecond)
re, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode)
assert.Equal(t, cbState(stateRecovering), cb.state)
// 5 seconds after we should be allowing some requests to pass
- clock.CurrentTime = clock.CurrentTime.Add(5 * time.Second)
+ clock.Advance(5 * clock.Second)
allowed := 0
for i := 0; i < 100; i++ {
re, _, err = testutils.Get(srv.URL)
@@ -83,7 +85,7 @@ func TestFullCycle(t *testing.T) {
assert.NotEqual(t, 0, allowed)
// After some time, all is good and we should be in stand by mode again
- clock.CurrentTime = clock.CurrentTime.Add(5*time.Second + time.Millisecond)
+ clock.Advance(5*clock.Second + clock.Millisecond)
re, _, err = testutils.Get(srv.URL)
assert.Equal(t, cbState(stateStandby), cb.state)
require.NoError(t, err)
@@ -92,7 +94,7 @@ func TestFullCycle(t *testing.T) {
func TestRedirectWithPath(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
fallbackRedirectPath, err := NewRedirectFallback(Redirect{
@@ -101,7 +103,7 @@ func TestRedirectWithPath(t *testing.T) {
})
require.NoError(t, err)
- cb, err := New(handler, triggerNetRatio, Clock(testutils.GetClock()), Fallback(fallbackRedirectPath))
+ cb, err := New(handler, triggerNetRatio, Fallback(fallbackRedirectPath))
require.NoError(t, err)
srv := httptest.NewServer(cb)
@@ -125,13 +127,13 @@ func TestRedirectWithPath(t *testing.T) {
func TestRedirect(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
fallbackRedirect, err := NewRedirectFallback(Redirect{URL: "http://localhost:5000"})
require.NoError(t, err)
- cb, err := New(handler, triggerNetRatio, Clock(testutils.GetClock()), Fallback(fallbackRedirect))
+ cb, err := New(handler, triggerNetRatio, Fallback(fallbackRedirect))
require.NoError(t, err)
srv := httptest.NewServer(cb)
@@ -155,12 +157,13 @@ func TestRedirect(t *testing.T) {
func TestTriggerDuringRecovery(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- cb, err := New(handler, triggerNetRatio, Clock(clock), CheckPeriod(time.Microsecond))
+ cb, err := New(handler, triggerNetRatio, CheckPeriod(clock.Microsecond))
require.NoError(t, err)
srv := httptest.NewServer(cb)
@@ -172,14 +175,14 @@ func TestTriggerDuringRecovery(t *testing.T) {
assert.Equal(t, cbState(stateTripped), cb.state)
// We should be in recovering state by now
- clock.CurrentTime = clock.CurrentTime.Add(10*time.Second + time.Millisecond)
+ clock.Advance(10*clock.Second + clock.Millisecond)
re, _, err := testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode)
assert.Equal(t, cbState(stateRecovering), cb.state)
// We have matched error condition during recovery state and are going back to tripped state
- clock.CurrentTime = clock.CurrentTime.Add(5 * time.Second)
+ clock.Advance(5 * clock.Second)
cb.metrics = statsNetErrors(0.6)
allowed := 0
for i := 0; i < 100; i++ {
@@ -196,17 +199,17 @@ func TestSideEffects(t *testing.T) {
srv1Chan := make(chan *http.Request, 1)
var srv1Body []byte
srv1 := testutils.NewHandler(func(w http.ResponseWriter, r *http.Request) {
- b, err := ioutil.ReadAll(r.Body)
+ b, err := io.ReadAll(r.Body)
require.NoError(t, err)
srv1Body = b
- w.Write([]byte("srv1"))
+ _, _ = w.Write([]byte("srv1"))
srv1Chan <- r
})
defer srv1.Close()
srv2Chan := make(chan *http.Request, 1)
srv2 := testutils.NewHandler(func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("srv2"))
+ _, _ = w.Write([]byte("srv2"))
err := r.ParseForm()
require.NoError(t, err)
srv2Chan <- r
@@ -231,12 +234,13 @@ func TestSideEffects(t *testing.T) {
require.NoError(t, err)
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- cb, err := New(handler, triggerNetRatio, Clock(clock), CheckPeriod(time.Microsecond), OnTripped(onTripped), OnStandby(onStandby))
+ cb, err := New(handler, triggerNetRatio, CheckPeriod(clock.Microsecond), OnTripped(onTripped), OnStandby(onStandby))
require.NoError(t, err)
srv := httptest.NewServer(cb)
@@ -254,19 +258,19 @@ func TestSideEffects(t *testing.T) {
assert.Equal(t, "/post.json", req.URL.Path)
assert.Equal(t, `{"Key": ["val1", "val2"]}`, string(srv1Body))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
- case <-time.After(time.Second):
+ case <-clock.After(clock.Second):
t.Error("timeout waiting for side effect to kick off")
}
// Transition to recovering state
- clock.CurrentTime = clock.CurrentTime.Add(10*time.Second + time.Millisecond)
+ clock.Advance(10*clock.Second + clock.Millisecond)
cb.metrics = statsOK()
_, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, cbState(stateRecovering), cb.state)
// Going back to standby
- clock.CurrentTime = clock.CurrentTime.Add(10*time.Second + time.Millisecond)
+ clock.Advance(10*clock.Second + clock.Millisecond)
_, _, err = testutils.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, cbState(stateStandby), cb.state)
@@ -276,7 +280,7 @@ func TestSideEffects(t *testing.T) {
assert.Equal(t, http.MethodPost, req.Method)
assert.Equal(t, "/post", req.URL.Path)
assert.Equal(t, url.Values{"key": []string{"val1", "val2"}}, req.Form)
- case <-time.After(time.Second):
+ case <-clock.After(clock.Second):
t.Error("timeout waiting for side effect to kick off")
}
}
diff --git a/cbreaker/effect.go b/cbreaker/effect.go
index 88aae14..36af754 100644
--- a/cbreaker/effect.go
+++ b/cbreaker/effect.go
@@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"io"
- "io/ioutil"
"net/http"
"net/url"
"strings"
@@ -13,12 +12,12 @@ import (
"github.com/vulcand/oxy/utils"
)
-// SideEffect a side effect
+// SideEffect a side effect.
type SideEffect interface {
Exec() error
}
-// Webhook Web hook
+// Webhook Web hook.
type Webhook struct {
URL string
Method string
@@ -27,14 +26,14 @@ type Webhook struct {
Body []byte
}
-// WebhookSideEffect a web hook side effect
+// WebhookSideEffect a web hook side effect.
type WebhookSideEffect struct {
w Webhook
log *log.Logger
}
-// NewWebhookSideEffectsWithLogger creates a new WebhookSideEffect
+// NewWebhookSideEffectsWithLogger creates a new WebhookSideEffect.
func NewWebhookSideEffectsWithLogger(w Webhook, l *log.Logger) (*WebhookSideEffect, error) {
if w.Method == "" {
return nil, fmt.Errorf("Supply method")
@@ -47,7 +46,7 @@ func NewWebhookSideEffectsWithLogger(w Webhook, l *log.Logger) (*WebhookSideEffe
return &WebhookSideEffect{w: w, log: l}, nil
}
-// NewWebhookSideEffect creates a new WebhookSideEffect
+// NewWebhookSideEffect creates a new WebhookSideEffect.
func NewWebhookSideEffect(w Webhook) (*WebhookSideEffect, error) {
return NewWebhookSideEffectsWithLogger(w, log.StandardLogger())
}
@@ -62,7 +61,7 @@ func (w *WebhookSideEffect) getBody() io.Reader {
return nil
}
-// Exec execute the side effect
+// Exec execute the side effect.
func (w *WebhookSideEffect) Exec() error {
r, err := http.NewRequest(w.w.Method, w.w.URL, w.getBody())
if err != nil {
@@ -81,7 +80,7 @@ func (w *WebhookSideEffect) Exec() error {
if re.Body != nil {
defer re.Body.Close()
}
- body, err := ioutil.ReadAll(re.Body)
+ body, err := io.ReadAll(re.Body)
if err != nil {
return err
}
diff --git a/cbreaker/fallback.go b/cbreaker/fallback.go
index 5c24548..ee8c0c7 100644
--- a/cbreaker/fallback.go
+++ b/cbreaker/fallback.go
@@ -10,21 +10,21 @@ import (
"github.com/vulcand/oxy/utils"
)
-// Response response model
+// Response response model.
type Response struct {
StatusCode int
ContentType string
Body []byte
}
-// ResponseFallback fallback response handler
+// ResponseFallback fallback response handler.
type ResponseFallback struct {
r Response
log *log.Logger
}
-// NewResponseFallbackWithLogger creates a new ResponseFallback
+// NewResponseFallbackWithLogger creates a new ResponseFallback.
func NewResponseFallbackWithLogger(r Response, l *log.Logger) (*ResponseFallback, error) {
if r.StatusCode == 0 {
return nil, fmt.Errorf("response code should not be 0")
@@ -32,14 +32,14 @@ func NewResponseFallbackWithLogger(r Response, l *log.Logger) (*ResponseFallback
return &ResponseFallback{r: r, log: l}, nil
}
-// NewResponseFallback creates a new ResponseFallback
+// NewResponseFallback creates a new ResponseFallback.
func NewResponseFallback(r Response) (*ResponseFallback, error) {
return NewResponseFallbackWithLogger(r, log.StandardLogger())
}
func (f *ResponseFallback) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if f.log.Level >= log.DebugLevel {
- logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/fallback/response: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/fallback/response: completed ServeHttp on request")
}
@@ -55,13 +55,13 @@ func (f *ResponseFallback) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
-// Redirect redirect model
+// Redirect redirect model.
type Redirect struct {
URL string
PreservePath bool
}
-// RedirectFallback fallback redirect handler
+// RedirectFallback fallback redirect handler.
type RedirectFallback struct {
r Redirect
@@ -70,7 +70,7 @@ type RedirectFallback struct {
log *log.Logger
}
-// NewRedirectFallbackWithLogger creates a new RedirectFallback
+// NewRedirectFallbackWithLogger creates a new RedirectFallback.
func NewRedirectFallbackWithLogger(r Redirect, l *log.Logger) (*RedirectFallback, error) {
u, err := url.ParseRequestURI(r.URL)
if err != nil {
@@ -79,14 +79,14 @@ func NewRedirectFallbackWithLogger(r Redirect, l *log.Logger) (*RedirectFallback
return &RedirectFallback{r: r, u: u, log: l}, nil
}
-// NewRedirectFallback creates a new RedirectFallback
+// NewRedirectFallback creates a new RedirectFallback.
func NewRedirectFallback(r Redirect) (*RedirectFallback, error) {
return NewRedirectFallbackWithLogger(r, log.StandardLogger())
}
func (f *RedirectFallback) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if f.log.Level >= log.DebugLevel {
- logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/fallback/redirect: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/fallback/redirect: completed ServeHttp on request")
}
diff --git a/cbreaker/predicates.go b/cbreaker/predicates.go
index a858daf..34a66ab 100644
--- a/cbreaker/predicates.go
+++ b/cbreaker/predicates.go
@@ -2,8 +2,8 @@ package cbreaker
import (
"fmt"
- "time"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/predicate"
)
@@ -43,6 +43,7 @@ func parseExpression(in string) (hpredicate, error) {
}
type toInt func(c *CircuitBreaker) int
+
type toFloat64 func(c *CircuitBreaker) float64
func latencyAtQuantile(quantile float64) toInt {
@@ -52,7 +53,7 @@ func latencyAtQuantile(quantile float64) toInt {
c.log.Errorf("Failed to get latency histogram, for %v error: %v", c, err)
return 0
}
- return int(h.LatencyAtQuantile(quantile) / time.Millisecond)
+ return int(h.LatencyAtQuantile(quantile) / clock.Millisecond)
}
}
@@ -68,7 +69,7 @@ func responseCodeRatio(startA, endA, startB, endB int) toFloat64 {
}
}
-// or returns predicate by joining the passed predicates with logical 'or'
+// or returns predicate by joining the passed predicates with logical 'or'.
func or(fns ...hpredicate) hpredicate {
return func(c *CircuitBreaker) bool {
for _, fn := range fns {
@@ -80,7 +81,7 @@ func or(fns ...hpredicate) hpredicate {
}
}
-// and returns predicate by joining the passed predicates with logical 'and'
+// and returns predicate by joining the passed predicates with logical 'and'.
func and(fns ...hpredicate) hpredicate {
return func(c *CircuitBreaker) bool {
for _, fn := range fns {
@@ -92,14 +93,14 @@ func and(fns ...hpredicate) hpredicate {
}
}
-// not creates negation of the passed predicate
+// not creates negation of the passed predicate.
func not(p hpredicate) hpredicate {
return func(c *CircuitBreaker) bool {
return !p(c)
}
}
-// eq returns predicate that tests for equality of the value of the mapper and the constant
+// eq returns predicate that tests for equality of the value of the mapper and the constant.
func eq(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
@@ -110,7 +111,7 @@ func eq(m interface{}, value interface{}) (hpredicate, error) {
return nil, fmt.Errorf("eq: unsupported argument: %T", m)
}
-// neq returns predicate that tests for inequality of the value of the mapper and the constant
+// neq returns predicate that tests for inequality of the value of the mapper and the constant.
func neq(m interface{}, value interface{}) (hpredicate, error) {
p, err := eq(m, value)
if err != nil {
@@ -119,7 +120,7 @@ func neq(m interface{}, value interface{}) (hpredicate, error) {
return not(p), nil
}
-// lt returns predicate that tests that value of the mapper function is less than the constant
+// lt returns predicate that tests that value of the mapper function is less than the constant.
func lt(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
@@ -130,7 +131,7 @@ func lt(m interface{}, value interface{}) (hpredicate, error) {
return nil, fmt.Errorf("lt: unsupported argument: %T", m)
}
-// le returns predicate that tests that value of the mapper function is less or equal than the constant
+// le returns predicate that tests that value of the mapper function is less or equal than the constant.
func le(m interface{}, value interface{}) (hpredicate, error) {
l, err := lt(m, value)
if err != nil {
@@ -145,7 +146,7 @@ func le(m interface{}, value interface{}) (hpredicate, error) {
}, nil
}
-// gt returns predicate that tests that value of the mapper function is greater than the constant
+// gt returns predicate that tests that value of the mapper function is greater than the constant.
func gt(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
@@ -156,7 +157,7 @@ func gt(m interface{}, value interface{}) (hpredicate, error) {
return nil, fmt.Errorf("gt: unsupported argument: %T", m)
}
-// ge returns predicate that tests that value of the mapper function is less or equal than the constant
+// ge returns predicate that tests that value of the mapper function is less or equal than the constant.
func ge(m interface{}, value interface{}) (hpredicate, error) {
g, err := gt(m, value)
if err != nil {
diff --git a/cbreaker/predicates_test.go b/cbreaker/predicates_test.go
index 524fb77..15e3827 100644
--- a/cbreaker/predicates_test.go
+++ b/cbreaker/predicates_test.go
@@ -2,10 +2,10 @@ package cbreaker
import (
"testing"
- "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/memmetrics"
)
@@ -27,12 +27,12 @@ func TestTripped(t *testing.T) {
},
{
expression: "LatencyAtQuantileMS(50.0) > 50",
- metrics: statsLatencyAtQuantile(50, time.Millisecond*51),
+ metrics: statsLatencyAtQuantile(50, clock.Millisecond*51),
expected: true,
},
{
expression: "LatencyAtQuantileMS(50.0) < 50",
- metrics: statsLatencyAtQuantile(50, time.Millisecond*51),
+ metrics: statsLatencyAtQuantile(50, clock.Millisecond*51),
expected: false,
},
{
diff --git a/cbreaker/ratio.go b/cbreaker/ratio.go
index 96f9eeb..7a12d64 100644
--- a/cbreaker/ratio.go
+++ b/cbreaker/ratio.go
@@ -4,8 +4,8 @@ import (
"fmt"
"time"
- "github.com/mailgun/timetools"
- log "github.com/sirupsen/logrus"
+ "github.com/sirupsen/logrus"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
// ratioController allows passing portions traffic back to the endpoints,
@@ -15,21 +15,18 @@ import (
//
type ratioController struct {
duration time.Duration
- start time.Time
- tm timetools.TimeProvider
+ start clock.Time
allowed int
denied int
- log *log.Logger
+ log *logrus.Logger
}
-func newRatioController(tm timetools.TimeProvider, rampUp time.Duration, log *log.Logger) *ratioController {
+func newRatioController(rampUp time.Duration, log *logrus.Logger) *ratioController {
return &ratioController{
duration: rampUp,
- tm: tm,
- start: tm.UtcNow(),
-
- log: log,
+ start: clock.Now().UTC(),
+ log: log,
}
}
@@ -70,5 +67,5 @@ func (r *ratioController) targetRatio() float64 {
// after this point to achieve ratio of 1 (that can never be reached unless d is 0)
// so we stop from there
multiplier := 0.5 / float64(r.duration)
- return multiplier * float64(r.tm.UtcNow().Sub(r.start))
+ return multiplier * float64(clock.Now().UTC().Sub(r.start))
}
diff --git a/cbreaker/ratio_test.go b/cbreaker/ratio_test.go
index c780f41..8a57e46 100644
--- a/cbreaker/ratio_test.go
+++ b/cbreaker/ratio_test.go
@@ -3,25 +3,29 @@ package cbreaker
import (
"math"
"testing"
- "time"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
func TestRampUp(t *testing.T) {
- clock := testutils.GetClock()
- duration := 10 * time.Second
- rc := newRatioController(clock, duration, log.StandardLogger())
+ done := testutils.FreezeTime()
+ defer done()
+ duration := 10 * clock.Second
+ rc := newRatioController(duration, log.StandardLogger())
allowed, denied := 0, 0
- for i := 0; i < int(duration/time.Millisecond); i++ {
+ for i := 0; i < int(duration/clock.Millisecond); i++ {
ratio := sendRequest(&allowed, &denied, rc)
expected := rc.targetRatio()
diff := math.Abs(expected - ratio)
+ t.Log("Ratio", ratio)
+ t.Log("Expected", expected)
+ t.Log("Diff", diff)
assert.EqualValues(t, 0, round(diff, 0.5, 1))
- clock.CurrentTime = clock.CurrentTime.Add(time.Millisecond)
+ clock.Advance(clock.Millisecond)
}
}
diff --git a/connlimit/connlimit.go b/connlimit/connlimit.go
index 5d2d714..3480f88 100644
--- a/connlimit/connlimit.go
+++ b/connlimit/connlimit.go
@@ -11,7 +11,7 @@ import (
)
// ConnLimiter tracks concurrent connection per token
-// and is capable of rejecting connections if they are failed
+// and is capable of rejecting connections if they are failed.
type ConnLimiter struct {
mutex *sync.Mutex
extract utils.SourceExtractor
@@ -24,8 +24,8 @@ type ConnLimiter struct {
log *log.Logger
}
-// New creates a new ConnLimiter
-func New(next http.Handler, extract utils.SourceExtractor, maxConnections int64, options ...ConnLimitOption) (*ConnLimiter, error) {
+// New creates a new ConnLimiter.
+func New(next http.Handler, extract utils.SourceExtractor, maxConnections int64, options ...Option) (*ConnLimiter, error) {
if extract == nil {
return nil, fmt.Errorf("Extract function can not be nil")
}
@@ -51,17 +51,7 @@ func New(next http.Handler, extract utils.SourceExtractor, maxConnections int64,
return cl, nil
}
-// Logger defines the logger the connection limiter will use.
-//
-// It defaults to logrus.StandardLogger(), the global logger used by logrus.
-func Logger(l *log.Logger) ConnLimitOption {
- return func(cl *ConnLimiter) error {
- cl.log = l
- return nil
- }
-}
-
-// Wrap sets the next handler to be called by connexion limiter handler.
+// Wrap sets the next handler to be called by connection limiter handler.
func (cl *ConnLimiter) Wrap(h http.Handler) {
cl.next = h
}
@@ -111,7 +101,7 @@ func (cl *ConnLimiter) release(token string, amount int64) {
}
}
-// MaxConnError maximum connections reached error
+// MaxConnError maximum connections reached error.
type MaxConnError struct {
max int64
}
@@ -120,31 +110,45 @@ func (m *MaxConnError) Error() string {
return fmt.Sprintf("max connections reached: %d", m.max)
}
-// ConnErrHandler connection limiter error handler
+// ConnErrHandler connection limiter error handler.
type ConnErrHandler struct {
log *log.Logger
}
func (e *ConnErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) {
if e.log.Level >= log.DebugLevel {
- logEntry := e.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := e.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/connlimit: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/connlimit: completed ServeHttp on request")
}
if _, ok := err.(*MaxConnError); ok {
w.WriteHeader(429)
- w.Write([]byte(err.Error()))
+ _, _ = w.Write([]byte(err.Error()))
return
}
utils.DefaultHandler.ServeHTTP(w, req, err)
}
-// ConnLimitOption connection limit option type
-type ConnLimitOption func(l *ConnLimiter) error
+// Logger defines the logger the connection limiter will use.
+//
+// It defaults to logrus.StandardLogger(), the global logger used by logrus.
+func Logger(l *log.Logger) Option {
+ return func(cl *ConnLimiter) error {
+ cl.log = l
+ return nil
+ }
+}
+
+// ConnLimitOption connection limit option type.
+// Deprecated: use Option instead.
+type ConnLimitOption = Option
+
+// Option connection limit option type.
+type Option func(l *ConnLimiter) error
-// ErrorHandler sets error handler of the server
-func ErrorHandler(h utils.ErrorHandler) ConnLimitOption {
+// ErrorHandler sets error handler of the server.
+func ErrorHandler(h utils.ErrorHandler) Option {
return func(cl *ConnLimiter) error {
cl.errHandler = h
return nil
diff --git a/connlimit/connlimit_test.go b/connlimit/connlimit_test.go
index 577122c..bd281e8 100644
--- a/connlimit/connlimit_test.go
+++ b/connlimit/connlimit_test.go
@@ -12,19 +12,19 @@ import (
"github.com/vulcand/oxy/utils"
)
-// We've hit the limit and were able to proceed once the request has completed
+// We've hit the limit and were able to proceed once the request has completed.
func TestHitLimitAndRelease(t *testing.T) {
wait := make(chan bool)
proceed := make(chan bool)
finish := make(chan bool)
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- fmt.Println(req.Header)
+ t.Logf("%v", req.Header)
if req.Header.Get("Wait") != "" {
proceed <- true
<-wait
}
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
cl, err := New(handler, headerLimit, 1)
@@ -60,15 +60,15 @@ func TestHitLimitAndRelease(t *testing.T) {
assert.Equal(t, http.StatusOK, re.StatusCode)
}
-// We've hit the limit and were able to proceed once the request has completed
+// We've hit the limit and were able to proceed once the request has completed.
func TestCustomHandlers(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
errHandler := utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
- w.Write([]byte(http.StatusText(http.StatusTeapot)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})
l, err := New(handler, headerLimit, 0, ErrorHandler(errHandler))
@@ -82,10 +82,10 @@ func TestCustomHandlers(t *testing.T) {
assert.Equal(t, http.StatusTeapot, re.StatusCode)
}
-// We've hit the limit and were able to proceed once the request has completed
+// We've hit the limit and were able to proceed once the request has completed.
func TestFaultyExtract(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
l, err := New(handler, faultyExtract, 1)
@@ -108,4 +108,5 @@ func faultyExtractor(_ *http.Request) (string, int64, error) {
}
var headerLimit = utils.ExtractorFunc(headerLimiter)
+
var faultyExtract = utils.ExtractorFunc(faultyExtractor)
diff --git a/debian/changelog b/debian/changelog
index 7d1d41b..eb86b98 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+golang-github-vulcand-oxy (1.4.2-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+ * Drop patch 01-fixes-test-32-bit-arch.patch, present upstream.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 28 Jan 2023 01:38:48 -0000
+
golang-github-vulcand-oxy (1.3.0-3) unstable; urgency=medium
* Write patch to fix test on 32 bit arch.
diff --git a/debian/patches/01-fixes-test-32-bit-arch.patch b/debian/patches/01-fixes-test-32-bit-arch.patch
deleted file mode 100644
index fcd4f29..0000000
--- a/debian/patches/01-fixes-test-32-bit-arch.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-Description: Fix tests on 32 bit arch
-Bug: https://github.com/vulcand/oxy/issues/219
-Bug-Debian: https://bugs.debian.org/994009
-Forwarded: https://github.com/vulcand/oxy/pull/220
-Author: Aloïs Micard <creekorful@debian.org>
-Last-Update: 2021-09-09
-
---- a/ratelimit/bucketset.go
-+++ b/ratelimit/bucketset.go
-@@ -89,11 +89,11 @@
- // debugState returns string that reflects the current state of all buckets in
- // this set. It is intended to be used for debugging and testing only.
- func (tbs *TokenBucketSet) debugState() string {
-- periods := sort.IntSlice(make([]int, 0, len(tbs.buckets)))
-+ periods := make([]int64, 0, len(tbs.buckets))
- for period := range tbs.buckets {
-- periods = append(periods, int(period))
-+ periods = append(periods, int64(period))
- }
-- sort.Sort(periods)
-+ sort.Slice(periods, func(i, j int) bool { return periods[i] < periods[j] })
- bucketRepr := make([]string, 0, len(tbs.buckets))
- for _, period := range periods {
- bucket := tbs.buckets[time.Duration(period)]
diff --git a/debian/patches/series b/debian/patches/series
index 45e4426..e69de29 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +0,0 @@
-01-fixes-test-32-bit-arch.patch
diff --git a/forward/fwd.go b/forward/fwd.go
index be39838..8f1cb1c 100644
--- a/forward/fwd.go
+++ b/forward/fwd.go
@@ -7,7 +7,6 @@ import (
"bytes"
"crypto/tls"
"errors"
- "fmt"
"io"
"net"
"net/http"
@@ -20,10 +19,11 @@ import (
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/utils"
)
-// OxyLogger interface of the internal
+// OxyLogger interface of the internal.
type OxyLogger interface {
log.FieldLogger
GetLevel() log.Level
@@ -37,14 +37,14 @@ func (i *internalLogger) GetLevel() log.Level {
return i.Level
}
-// ReqRewriter can alter request headers and body
+// ReqRewriter can alter request headers and body.
type ReqRewriter interface {
Rewrite(r *http.Request)
}
type optSetter func(f *Forwarder) error
-// PassHostHeader specifies if a client's Host header field should be delegated
+// PassHostHeader specifies if a client's Host header field should be delegated.
func PassHostHeader(b bool) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.passHost = b
@@ -53,7 +53,7 @@ func PassHostHeader(b bool) optSetter {
}
// RoundTripper sets a new http.RoundTripper
-// Forwarder will use http.DefaultTransport as a default round tripper
+// Forwarder will use http.DefaultTransport as a default round tripper.
func RoundTripper(r http.RoundTripper) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.roundTripper = r
@@ -61,7 +61,7 @@ func RoundTripper(r http.RoundTripper) optSetter {
}
}
-// Rewriter defines a request rewriter for the HTTP forwarder
+// Rewriter defines a request rewriter for the HTTP forwarder.
func Rewriter(r ReqRewriter) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.rewriter = r
@@ -69,7 +69,7 @@ func Rewriter(r ReqRewriter) optSetter {
}
}
-// WebsocketTLSClientConfig define the websocker client TLS configuration
+// WebsocketTLSClientConfig define the websocker client TLS configuration.
func WebsocketTLSClientConfig(tcc *tls.Config) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.tlsClientConfig = tcc
@@ -77,7 +77,7 @@ func WebsocketTLSClientConfig(tcc *tls.Config) optSetter {
}
}
-// ErrorHandler is a functional argument that sets error handler of the server
+// ErrorHandler is a functional argument that sets error handler of the server.
func ErrorHandler(h utils.ErrorHandler) optSetter {
return func(f *Forwarder) error {
f.errHandler = h
@@ -120,15 +120,15 @@ func Logger(l log.FieldLogger) optSetter {
}
}
-// StateListener defines a state listener for the HTTP forwarder
-func StateListener(stateListener UrlForwardingStateListener) optSetter {
+// StateListener defines a state listener for the HTTP forwarder.
+func StateListener(stateListener URLForwardingStateListener) optSetter {
return func(f *Forwarder) error {
f.stateListener = stateListener
return nil
}
}
-// WebsocketConnectionClosedHook defines a hook called when websocket connection is closed
+// WebsocketConnectionClosedHook defines a hook called when websocket connection is closed.
func WebsocketConnectionClosedHook(hook func(req *http.Request, conn net.Conn)) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.websocketConnectionClosedHook = hook
@@ -136,7 +136,7 @@ func WebsocketConnectionClosedHook(hook func(req *http.Request, conn net.Conn))
}
}
-// ResponseModifier defines a response modifier for the HTTP forwarder
+// ResponseModifier defines a response modifier for the HTTP forwarder.
func ResponseModifier(responseModifier func(*http.Response) error) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.modifyResponse = responseModifier
@@ -144,7 +144,7 @@ func ResponseModifier(responseModifier func(*http.Response) error) optSetter {
}
}
-// StreamingFlushInterval defines a streaming flush interval for the HTTP forwarder
+// StreamingFlushInterval defines a streaming flush interval for the HTTP forwarder.
func StreamingFlushInterval(flushInterval time.Duration) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.flushInterval = flushInterval
@@ -153,21 +153,20 @@ func StreamingFlushInterval(flushInterval time.Duration) optSetter {
}
// Forwarder wraps two traffic forwarding implementations: HTTP and websockets.
-// It decides based on the specified request which implementation to use
+// It decides based on the specified request which implementation to use.
type Forwarder struct {
*httpForwarder
*handlerContext
- stateListener UrlForwardingStateListener
+ stateListener URLForwardingStateListener
stream bool
}
-// handlerContext defines a handler context for error reporting and logging
+// handlerContext defines a handler context for error reporting and logging.
type handlerContext struct {
errHandler utils.ErrorHandler
}
-// httpForwarder is a handler that can reverse proxy
-// HTTP traffic
+// httpForwarder is a handler that can reverse proxy HTTP traffic.
type httpForwarder struct {
roundTripper http.RoundTripper
rewriter ReqRewriter
@@ -183,18 +182,22 @@ type httpForwarder struct {
websocketConnectionClosedHook func(req *http.Request, conn net.Conn)
}
-const defaultFlushInterval = time.Duration(100) * time.Millisecond
+const defaultFlushInterval = 100 * clock.Millisecond
-// Connection states
+// Connection states.
const (
StateConnected = iota
StateDisconnected
)
-// UrlForwardingStateListener URL forwarding state listener
-type UrlForwardingStateListener func(*url.URL, int)
+// UrlForwardingStateListener alias on URLForwardingStateListener.
+// Deprecated: use URLForwardingStateListener instead.
+type UrlForwardingStateListener = URLForwardingStateListener
-// New creates an instance of Forwarder based on the provided list of configuration options
+// URLForwardingStateListener URL forwarding state listener.
+type URLForwardingStateListener func(*url.URL, int)
+
+// New creates an instance of Forwarder based on the provided list of configuration options.
func New(setters ...optSetter) (*Forwarder, error) {
f := &Forwarder{
httpForwarder: &httpForwarder{log: &internalLogger{Logger: log.StandardLogger()}},
@@ -240,10 +243,10 @@ func New(setters ...optSetter) (*Forwarder, error) {
}
// ServeHTTP decides which forwarder to use based on the specified
-// request and delegates to the proper implementation
+// request and delegates to the proper implementation.
func (f *Forwarder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if f.log.GetLevel() >= log.DebugLevel {
- logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/forward: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/forward: completed ServeHttp on request")
}
@@ -259,7 +262,7 @@ func (f *Forwarder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
-func (f *httpForwarder) getUrlFromRequest(req *http.Request) *url.URL {
+func (f *httpForwarder) getURLFromRequest(req *http.Request) *url.URL {
// If the Request was created by Go via a real HTTP request, RequestURI will
// contain the original query string. If the Request was created in code, RequestURI
// will be empty, and we will use the URL object instead
@@ -275,13 +278,13 @@ func (f *httpForwarder) getUrlFromRequest(req *http.Request) *url.URL {
return u
}
-// Modify the request to handle the target URL
+// Modify the request to handle the target URL.
func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
outReq.URL = utils.CopyURL(outReq.URL)
outReq.URL.Scheme = target.Scheme
outReq.URL.Host = target.Host
- u := f.getUrlFromRequest(outReq)
+ u := f.getURLFromRequest(outReq)
outReq.URL.Path = u.Path
outReq.URL.RawPath = u.RawPath
@@ -302,10 +305,10 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
}
}
-// serveWebSocket forwards websocket traffic
+// serveWebSocket forwards websocket traffic.
func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, ctx *handlerContext) {
if f.log.GetLevel() >= log.DebugLevel {
- logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/forward/websocket: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/forward/websocket: completed ServeHttp on request")
}
@@ -313,45 +316,47 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
outReq := f.copyWebSocketRequest(req)
dialer := websocket.DefaultDialer
-
if outReq.URL.Scheme == "wss" && f.tlsClientConfig != nil {
dialer.TLSClientConfig = f.tlsClientConfig.Clone()
// WebSocket is only in http/1.1
dialer.TLSClientConfig.NextProtos = []string{"http/1.1"}
}
+
targetConn, resp, err := dialer.DialContext(outReq.Context(), outReq.URL.String(), outReq.Header)
if err != nil {
if resp == nil {
ctx.errHandler.ServeHTTP(w, req, err)
- } else {
- f.log.Errorf("vulcand/oxy/forward/websocket: Error dialing %q: %v with resp: %d %s", outReq.Host, err, resp.StatusCode, resp.Status)
- hijacker, ok := w.(http.Hijacker)
- if !ok {
- f.log.Errorf("vulcand/oxy/forward/websocket: %s can not be hijack", reflect.TypeOf(w))
- ctx.errHandler.ServeHTTP(w, req, err)
- return
- }
+ return
+ }
- conn, _, errHijack := hijacker.Hijack()
- if errHijack != nil {
- f.log.Errorf("vulcand/oxy/forward/websocket: Failed to hijack responseWriter")
- ctx.errHandler.ServeHTTP(w, req, errHijack)
- return
- }
- defer func() {
- conn.Close()
- if f.websocketConnectionClosedHook != nil {
- f.websocketConnectionClosedHook(req, conn)
- }
- }()
+ f.log.Errorf("vulcand/oxy/forward/websocket: Error dialing %q: %v with resp: %d %s", outReq.Host, err, resp.StatusCode, resp.Status)
+ hijacker, ok := w.(http.Hijacker)
+ if !ok {
+ f.log.Errorf("vulcand/oxy/forward/websocket: %s can not be hijack", reflect.TypeOf(w))
+ ctx.errHandler.ServeHTTP(w, req, err)
+ return
+ }
- errWrite := resp.Write(conn)
- if errWrite != nil {
- f.log.Errorf("vulcand/oxy/forward/websocket: Failed to forward response")
- ctx.errHandler.ServeHTTP(w, req, errWrite)
- return
+ conn, _, errHijack := hijacker.Hijack()
+ if errHijack != nil {
+ f.log.Errorf("vulcand/oxy/forward/websocket: Failed to hijack responseWriter")
+ ctx.errHandler.ServeHTTP(w, req, errHijack)
+ return
+ }
+ defer func() {
+ _ = conn.Close()
+ if f.websocketConnectionClosedHook != nil {
+ f.websocketConnectionClosedHook(req, conn)
}
+ }()
+
+ errWrite := resp.Write(conn)
+ if errWrite != nil {
+ f.log.Errorf("vulcand/oxy/forward/websocket: Failed to forward response")
+ ctx.errHandler.ServeHTTP(w, req, errWrite)
+ return
}
+
return
}
@@ -369,8 +374,8 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
return
}
defer func() {
- underlyingConn.Close()
- targetConn.Close()
+ _ = underlyingConn.Close()
+ _ = targetConn.Close()
if f.websocketConnectionClosedHook != nil {
f.websocketConnectionClosedHook(req, underlyingConn.UnderlyingConn())
}
@@ -379,7 +384,6 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
errClient := make(chan error, 1)
errBackend := make(chan error, 1)
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) {
-
forward := func(messageType int, reader io.Reader) error {
writer, err := dst.NextWriter(messageType)
if err != nil {
@@ -402,24 +406,21 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
for {
msgType, reader, err := src.NextReader()
-
if err != nil {
- m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
+ m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error())
if e, ok := err.(*websocket.CloseError); ok {
if e.Code != websocket.CloseNoStatusReceived {
m = nil
// Following codes are not valid on the wire so just close the
// underlying TCP connection without sending a close frame.
- if e.Code != websocket.CloseAbnormalClosure &&
- e.Code != websocket.CloseTLSHandshake {
-
+ if e.Code != websocket.CloseAbnormalClosure && e.Code != websocket.CloseTLSHandshake {
m = websocket.FormatCloseMessage(e.Code, e.Text)
}
}
}
errc <- err
if m != nil {
- forward(websocket.CloseMessage, bytes.NewReader([]byte(m)))
+ _ = forward(websocket.CloseMessage, bytes.NewReader(m))
}
break
}
@@ -440,8 +441,8 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
message = "vulcand/oxy/forward/websocket: Error when copying from backend to client: %v"
case err = <-errBackend:
message = "vulcand/oxy/forward/websocket: Error when copying from client to backend: %v"
-
}
+
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
f.log.Errorf(message, err)
}
@@ -463,7 +464,7 @@ func (f *httpForwarder) copyWebSocketRequest(req *http.Request) (outReq *http.Re
outReq.URL.Scheme = "ws"
}
- u := f.getUrlFromRequest(outReq)
+ u := f.getURLFromRequest(outReq)
outReq.URL.Path = u.Path
outReq.URL.RawPath = u.RawPath
@@ -487,15 +488,15 @@ func (f *httpForwarder) copyWebSocketRequest(req *http.Request) (outReq *http.Re
return outReq
}
-// serveHTTP forwards HTTP traffic using the configured transport
+// serveHTTP forwards HTTP traffic using the configured transport.
func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ctx *handlerContext) {
if f.log.GetLevel() >= log.DebugLevel {
- logEntry := f.log.WithField("Request", utils.DumpHttpRequest(inReq))
+ logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(inReq))
logEntry.Debug("vulcand/oxy/forward/http: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/forward/http: completed ServeHttp on request")
}
- start := time.Now().UTC()
+ start := clock.Now().UTC()
outReq := new(http.Request)
*outReq = *inReq // includes shallow copies of maps, but we handle this in Director
@@ -517,14 +518,14 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ct
if inReq.TLS != nil {
f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v",
- inReq.URL, pw.StatusCode(), pw.GetLength(), time.Now().UTC().Sub(start),
+ inReq.URL, pw.StatusCode(), pw.GetLength(), clock.Now().UTC().Sub(start),
inReq.TLS.Version,
inReq.TLS.DidResume,
inReq.TLS.CipherSuite,
inReq.TLS.ServerName)
} else {
f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v",
- inReq.URL, pw.StatusCode(), pw.GetLength(), time.Now().UTC().Sub(start))
+ inReq.URL, pw.StatusCode(), pw.GetLength(), clock.Now().UTC().Sub(start))
}
} else {
revproxy.ServeHTTP(w, outReq)
@@ -538,11 +539,9 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ct
break
}
}
-
}
-// IsWebsocketRequest determines if the specified HTTP request is a
-// websocket handshake request
+// IsWebsocketRequest determines if the specified HTTP request is a websocket handshake request.
func IsWebsocketRequest(req *http.Request) bool {
containsHeader := func(name, value string) bool {
items := strings.Split(req.Header.Get(name), ",")
diff --git a/forward/fwd_chunked_go1_15_test.go b/forward/fwd_chunked_go1_15_test.go
index d7bc5b6..16ed56b 100644
--- a/forward/fwd_chunked_go1_15_test.go
+++ b/forward/fwd_chunked_go1_15_test.go
@@ -1,3 +1,4 @@
+//go:build !go1.16
// +build !go1.16
package forward
diff --git a/forward/fwd_chunked_test.go b/forward/fwd_chunked_test.go
index ee1ee06..80ff1ed 100644
--- a/forward/fwd_chunked_test.go
+++ b/forward/fwd_chunked_test.go
@@ -1,3 +1,4 @@
+//go:build go1.16
// +build go1.16
package forward
diff --git a/forward/fwd_test.go b/forward/fwd_test.go
index 07c0fdb..25c87f6 100644
--- a/forward/fwd_test.go
+++ b/forward/fwd_test.go
@@ -2,20 +2,20 @@ package forward
import (
"context"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
- "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
"github.com/vulcand/oxy/utils"
)
-// Makes sure hop-by-hop headers are removed
+// Makes sure hop-by-hop headers are removed.
func TestForwardHopHeaders(t *testing.T) {
called := false
var outHeaders http.Header
@@ -24,7 +24,7 @@ func TestForwardHopHeaders(t *testing.T) {
called = true
outHeaders = req.Header
outHost = req.Host
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -71,7 +71,7 @@ func TestDefaultErrHandler(t *testing.T) {
func TestCustomErrHandler(t *testing.T) {
f, err := New(ErrorHandler(utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
- w.Write([]byte(http.StatusText(http.StatusTeapot)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})))
require.NoError(t, err)
@@ -89,7 +89,7 @@ func TestCustomErrHandler(t *testing.T) {
func TestResponseModifier(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -115,22 +115,22 @@ func TestXForwardedHostHeader(t *testing.T) {
tests := []struct {
Description string
PassHostHeader bool
- TargetUrl string
- ProxyfiedUrl string
+ TargetURL string
+ ProxyfiedURL string
ExpectedXForwardedHost string
}{
{
Description: "XForwardedHost without PassHostHeader",
PassHostHeader: false,
- TargetUrl: "http://xforwardedhost.com",
- ProxyfiedUrl: "http://backend.com",
+ TargetURL: "http://xforwardedhost.com",
+ ProxyfiedURL: "http://backend.com",
ExpectedXForwardedHost: "xforwardedhost.com",
},
{
Description: "XForwardedHost with PassHostHeader",
PassHostHeader: true,
- TargetUrl: "http://xforwardedhost.com",
- ProxyfiedUrl: "http://backend.com",
+ TargetURL: "http://xforwardedhost.com",
+ ProxyfiedURL: "http://backend.com",
ExpectedXForwardedHost: "xforwardedhost.com",
},
}
@@ -143,22 +143,22 @@ func TestXForwardedHostHeader(t *testing.T) {
f, err := New(PassHostHeader(test.PassHostHeader))
require.NoError(t, err)
- r, err := http.NewRequest(http.MethodGet, test.TargetUrl, nil)
+ r, err := http.NewRequest(http.MethodGet, test.TargetURL, nil)
require.NoError(t, err)
- backendUrl, err := url.Parse(test.ProxyfiedUrl)
+ backendURL, err := url.Parse(test.ProxyfiedURL)
require.NoError(t, err)
- f.modifyRequest(r, backendUrl)
+ f.modifyRequest(r, backendURL)
require.Equal(t, test.ExpectedXForwardedHost, r.Header.Get(XForwardedHost))
})
}
}
-// Makes sure hop-by-hop headers are removed
+// Makes sure hop-by-hop headers are removed.
func TestForwardedHeaders(t *testing.T) {
var outHeaders http.Header
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
outHeaders = req.Header
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -191,7 +191,7 @@ func TestCustomRewriter(t *testing.T) {
var outHeaders http.Header
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
outHeaders = req.Header
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -218,14 +218,14 @@ func TestCustomRewriter(t *testing.T) {
func TestCustomTransportTimeout(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- time.Sleep(20 * time.Millisecond)
- w.Write([]byte("hello"))
+ clock.Sleep(20 * clock.Millisecond)
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New(RoundTripper(
&http.Transport{
- ResponseHeaderTimeout: 5 * time.Millisecond,
+ ResponseHeaderTimeout: 5 * clock.Millisecond,
}))
require.NoError(t, err)
@@ -242,7 +242,7 @@ func TestCustomTransportTimeout(t *testing.T) {
func TestCustomLogger(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -264,7 +264,7 @@ func TestRouteForwarding(t *testing.T) {
var outPath string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
outPath = req.RequestURI
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -309,7 +309,7 @@ func TestForwardedProto(t *testing.T) {
var proto string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
proto = req.Header.Get(XForwardedProto)
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -331,12 +331,14 @@ func TestForwardedProto(t *testing.T) {
}
func TestContextWithValueInErrHandler(t *testing.T) {
- var originalPBool *bool
originalBool := false
- originalPBool = &originalBool
+ originalPBool := &originalBool
+
+ type MyKey string
+ const key MyKey = "test"
f, err := New(ErrorHandler(utils.ErrorHandlerFunc(func(rw http.ResponseWriter, req *http.Request, err error) {
- test, isBool := req.Context().Value("test").(*bool)
+ test, isBool := req.Context().Value(key).(*bool)
if isBool {
*test = true
}
@@ -349,13 +351,15 @@ func TestContextWithValueInErrHandler(t *testing.T) {
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
// We need a network error
req.URL = testutils.ParseURI("http://localhost:63450")
- newReq := req.WithContext(context.WithValue(req.Context(), "test", originalPBool))
+ newReq := req.WithContext(context.WithValue(req.Context(), key, originalPBool))
+
f.ServeHTTP(w, newReq)
})
defer proxy.Close()
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
+
assert.Equal(t, http.StatusBadGateway, re.StatusCode)
assert.True(t, *originalPBool)
}
@@ -364,7 +368,7 @@ func TestTeTrailer(t *testing.T) {
var teHeader string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
teHeader = req.Header.Get(Te)
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -394,7 +398,7 @@ func TestUnannouncedTrailer(t *testing.T) {
}))
proxy, err := New()
- require.Nil(t, err)
+ require.NoError(t, err)
proxySrv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
@@ -402,7 +406,8 @@ func TestUnannouncedTrailer(t *testing.T) {
}))
resp, _ := http.Get(proxySrv.URL)
- ioutil.ReadAll(resp.Body)
+ _, err = io.ReadAll(resp.Body)
+ require.NoError(t, err)
require.Equal(t, resp.Trailer.Get("X-Trailer"), "foo")
}
diff --git a/forward/fwd_websocket_test.go b/forward/fwd_websocket_test.go
index 35ed6b4..8df6aad 100644
--- a/forward/fwd_websocket_test.go
+++ b/forward/fwd_websocket_test.go
@@ -9,11 +9,11 @@ import (
"net/http/httptest"
"runtime"
"testing"
- "time"
gorillawebsocket "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
"golang.org/x/net/websocket"
)
@@ -29,7 +29,7 @@ func TestWebSocketTCPClose(t *testing.T) {
if err != nil {
return
}
- defer c.Close()
+ defer func(c *gorillawebsocket.Conn) { _ = c.Close() }(c)
for {
_, _, err := c.ReadMessage()
if err != nil {
@@ -48,7 +48,7 @@ func TestWebSocketTCPClose(t *testing.T) {
withPath("/ws"),
).open()
require.NoError(t, err)
- conn.Close()
+ _ = conn.Close()
serverErr := <-errChan
@@ -67,9 +67,9 @@ func TestWebsocketConnectionClosedHook(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
- conn.Read(msg)
- conn.Write(msg)
- conn.Close()
+ _, _ = conn.Read(msg)
+ _, _ = conn.Write(msg)
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
@@ -92,16 +92,15 @@ func TestWebsocketConnectionClosedHook(t *testing.T) {
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
- conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
- fmt.Println(conn.ReadMessage())
+ _ = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
+ t.Log(conn.ReadMessage())
- conn.Close()
+ _ = conn.Close()
select {
- case <-time.After(time.Second):
+ case <-clock.After(clock.Second):
t.Errorf("Websocket Hook not called")
case <-closed:
-
}
}
@@ -109,8 +108,8 @@ func TestWebSocketPingPong(t *testing.T) {
f, err := New()
require.NoError(t, err)
- var upgrader = gorillawebsocket.Upgrader{
- HandshakeTimeout: 10 * time.Second,
+ upgrader := gorillawebsocket.Upgrader{
+ HandshakeTimeout: 10 * clock.Second,
CheckOrigin: func(*http.Request) bool {
return true
},
@@ -122,11 +121,11 @@ func TestWebSocketPingPong(t *testing.T) {
require.NoError(t, err)
ws.SetPingHandler(func(appData string) error {
- ws.WriteMessage(gorillawebsocket.PongMessage, []byte(appData+"Pong"))
+ _ = ws.WriteMessage(gorillawebsocket.PongMessage, []byte(appData+"Pong"))
return nil
})
- ws.ReadMessage()
+ _, _, _ = ws.ReadMessage()
})
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
@@ -159,7 +158,7 @@ func TestWebSocketPingPong(t *testing.T) {
return badErr
})
- conn.WriteControl(gorillawebsocket.PingMessage, []byte("Ping"), time.Now().Add(time.Second))
+ _ = conn.WriteControl(gorillawebsocket.PingMessage, []byte("Ping"), clock.Now().Add(clock.Second))
_, _, err = conn.ReadMessage()
if err != goodErr {
@@ -174,10 +173,10 @@ func TestWebSocketEcho(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
- conn.Read(msg)
- fmt.Println(string(msg))
- conn.Write(msg)
- conn.Close()
+ _, _ = conn.Read(msg)
+ t.Log(string(msg))
+ _, _ = conn.Write(msg)
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
@@ -200,10 +199,10 @@ func TestWebSocketEcho(t *testing.T) {
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
- conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
- fmt.Println(conn.ReadMessage())
+ _ = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
+ t.Log(conn.ReadMessage())
- conn.Close()
+ _ = conn.Close()
}
func TestWebSocketPassHost(t *testing.T) {
@@ -241,10 +240,10 @@ func TestWebSocketPassHost(t *testing.T) {
}
msg := make([]byte, 4)
- conn.Read(msg)
- fmt.Println(string(msg))
- conn.Write(msg)
- conn.Close()
+ _, _ = conn.Read(msg)
+ t.Log(string(msg))
+ _, _ = conn.Write(msg)
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
@@ -268,10 +267,10 @@ func TestWebSocketPassHost(t *testing.T) {
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
- conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
- fmt.Println(conn.ReadMessage())
+ _ = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
+ t.Log(conn.ReadMessage())
- conn.Close()
+ _ = conn.Close()
})
}
}
@@ -284,10 +283,10 @@ func TestWebSocketNumGoRoutine(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
- conn.Read(msg)
- fmt.Println(string(msg))
- conn.Write(msg)
- conn.Close()
+ _, _ = conn.Read(msg)
+ t.Log(string(msg))
+ _, _ = conn.Write(msg)
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
@@ -312,12 +311,12 @@ func TestWebSocketNumGoRoutine(t *testing.T) {
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
- conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
- fmt.Println(conn.ReadMessage())
+ _ = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
+ t.Log(conn.ReadMessage())
- conn.Close()
+ _ = conn.Close()
- time.Sleep(time.Second)
+ clock.Sleep(clock.Second)
assert.Equal(t, num, runtime.NumGoroutine())
}
@@ -454,7 +453,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
- conn.Close()
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
@@ -523,8 +522,8 @@ func TestWebSocketRequestWithEncodedChar(t *testing.T) {
func TestDetectsWebSocketRequest(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
- conn.Write([]byte("ok"))
- conn.Close()
+ _, _ = conn.Write([]byte("ok"))
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
websocketRequest := IsWebsocketRequest(req)
@@ -586,7 +585,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
req.Header.Add("upgrade", "websocket")
req.Header.Add("Connection", "upgrade")
- req.Write(conn)
+ _ = req.Write(conn)
// First request works with 400
br := bufio.NewReader(conn)
@@ -602,8 +601,8 @@ func TestForwardsWebsocketTraffic(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
- conn.Write([]byte("ok"))
- conn.Close()
+ _, _ = conn.Write([]byte("ok"))
+ _ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
@@ -646,11 +645,11 @@ func createTLSWebsocketServer() *httptest.Server {
return srv
}
-func createProxyWithForwarder(forwarder *Forwarder, URL string) *httptest.Server {
+func createProxyWithForwarder(forwarder *Forwarder, uri string) *httptest.Server {
return testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
// Set new backend URL
- req.URL = testutils.ParseURI(URL)
+ req.URL = testutils.ParseURI(uri)
req.URL.Path = path
forwarder.ServeHTTP(w, req)
@@ -717,7 +716,7 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
assert.Equal(t, "ok", resp)
}
-const dialTimeout = time.Second
+const dialTimeout = clock.Second
type websocketRequestOpt func(w *websocketRequest)
@@ -776,7 +775,8 @@ func (w *websocketRequest) send() (string, error) {
if _, err := conn.Write([]byte(w.Data)); err != nil {
return "", err
}
- var msg = make([]byte, 512)
+
+ msg := make([]byte, 512)
var n int
n, err = conn.Read(msg)
if err != nil {
diff --git a/forward/headers.go b/forward/headers.go
index 512e284..565794a 100644
--- a/forward/headers.go
+++ b/forward/headers.go
@@ -1,6 +1,6 @@
package forward
-// Headers
+// Headers.
const (
XForwardedProto = "X-Forwarded-Proto"
XForwardedFor = "X-Forwarded-For"
@@ -25,7 +25,7 @@ const (
// HopHeaders Hop-by-hop headers. These are removed when sent to the backend.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
-// Copied from reverseproxy.go, too bad
+// Copied from reverseproxy.go, too bad.
var HopHeaders = []string{
Connection,
KeepAlive,
@@ -37,7 +37,7 @@ var HopHeaders = []string{
Upgrade,
}
-// WebsocketDialHeaders Websocket dial headers
+// WebsocketDialHeaders Websocket dial headers.
var WebsocketDialHeaders = []string{
Upgrade,
Connection,
@@ -47,7 +47,7 @@ var WebsocketDialHeaders = []string{
SecWebsocketAccept,
}
-// WebsocketUpgradeHeaders Websocket upgrade headers
+// WebsocketUpgradeHeaders Websocket upgrade headers.
var WebsocketUpgradeHeaders = []string{
Upgrade,
Connection,
@@ -55,7 +55,7 @@ var WebsocketUpgradeHeaders = []string{
SecWebsocketExtensions,
}
-// XHeaders X-* headers
+// XHeaders X-* headers.
var XHeaders = []string{
XForwardedProto,
XForwardedFor,
diff --git a/forward/post_config.go b/forward/post_config.go
index 1c4b123..56e03d1 100644
--- a/forward/post_config.go
+++ b/forward/post_config.go
@@ -1,3 +1,4 @@
+//go:build go1.11
// +build go1.11
package forward
diff --git a/forward/post_config_18.go b/forward/post_config_18.go
index 7fee684..2e85835 100644
--- a/forward/post_config_18.go
+++ b/forward/post_config_18.go
@@ -1,3 +1,4 @@
+//go:build !go1.11
// +build !go1.11
package forward
diff --git a/forward/rewrite.go b/forward/rewrite.go
index b5f8da1..0611174 100644
--- a/forward/rewrite.go
+++ b/forward/rewrite.go
@@ -8,18 +8,18 @@ import (
"github.com/vulcand/oxy/utils"
)
-// HeaderRewriter is responsible for removing hop-by-hop headers and setting forwarding headers
+// HeaderRewriter is responsible for removing hop-by-hop headers and setting forwarding headers.
type HeaderRewriter struct {
TrustForwardHeader bool
Hostname string
}
-// clean up IP in case if it is ipv6 address and it has {zone} information in it, like "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692"
+// clean up IP in case if it is ipv6 address and it has {zone} information in it, like "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692".
func ipv6fix(clientIP string) string {
return strings.Split(clientIP, "%")[0]
}
-// Rewrite rewrite request headers
+// Rewrite rewrite request headers.
func (rw *HeaderRewriter) Rewrite(req *http.Request) {
if !rw.TrustForwardHeader {
utils.RemoveHeaders(req.Header, XHeaders...)
diff --git a/go.mod b/go.mod
index 4dcf783..dc721f2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,22 +1,25 @@
module github.com/vulcand/oxy
-go 1.14
+go 1.17
require (
- github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd
- github.com/gorilla/websocket v1.4.2
- github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f // indirect
- github.com/jonboulle/clockwork v0.1.0 // indirect
- github.com/kr/pretty v0.1.0 // indirect
- github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f // indirect
- github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb
- github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51
- github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f
+ github.com/HdrHistogram/hdrhistogram-go v1.1.2
+ github.com/gorilla/websocket v1.5.0
+ github.com/mailgun/multibuf v0.1.2
github.com/segmentio/fasthash v1.0.3
- github.com/sirupsen/logrus v1.4.2
- github.com/stretchr/testify v1.5.1
- github.com/vulcand/predicate v1.1.0
- golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
- gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
- launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
+ github.com/sirupsen/logrus v1.8.1
+ github.com/stretchr/testify v1.7.1
+ github.com/vulcand/predicate v1.2.0
+ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect
+ github.com/jonboulle/clockwork v0.2.2 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
+ golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect
+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
diff --git a/go.sum b/go.sum
index 98dcf20..d2b0757 100644
--- a/go.sum
+++ b/go.sum
@@ -1,54 +1,142 @@
-github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
-github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/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/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f h1:68WxnfBzJRYktZ30fmIjGQ74RsXYLoeH2/NITPktTMY=
-github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
-github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf h1:C1GPyPJrOlJlIrcaBBiBpDsqZena2Ks8spa5xZqr1XQ=
+github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf/go.mod h1:zXqxTI6jXDdKnlf8s+nT+3c8LrwUEy3yNpO4XJL90lA=
+github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f h1:aOqSQstfwSx9+tcM/xiKTio3IVjs7ZL2vU8kI9bI6bM=
-github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f/go.mod h1:V3EvCedtJTvUYzJF2GZMRB0JMlai+6cBu3VCTQz33GQ=
-github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb h1:m2FGM8K2LC9Zyt/7zbQNn5Uvf/YV7vFWKtoMcC7hHU8=
-github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb/go.mod h1:E0vRBBIQUHcRtmL/oR6w/jehh4FJqJFxe86gBnw9gXc=
-github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51 h1:Kg/NPZLLC3aAFr1YToMs98dbCdhootQ1hZIvZU28hAQ=
-github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4=
-github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f h1:ZZYhg16XocqSKPGNQAe0aeweNtFxuedbwwb4fSlg7h4=
-github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f/go.mod h1:8heskWJ5c0v5J9WH89ADhyal1DOZcayll8fSbhB+/9A=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailgun/multibuf v0.1.2 h1:QE9kE27lK6LFZB4aYNVtUPlWVHVCT0zpgUr2uoq/+jk=
+github.com/mailgun/multibuf v0.1.2/go.mod h1:E+sUhIy69qgT6EM57kCPdUTlHnjTuxQBO/yf6af9Hes=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg=
-github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
+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/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50=
+github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 h1:8IVLkfbr2cLhv0a/vKq4UFUcJym8RmDoDboxCFWEjYE=
+golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=
-launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/internal/holsterv4/LICENSE b/internal/holsterv4/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/internal/holsterv4/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/internal/holsterv4/README.md b/internal/holsterv4/README.md
new file mode 100644
index 0000000..2a87deb
--- /dev/null
+++ b/internal/holsterv4/README.md
@@ -0,0 +1,24 @@
+# What is this?
+
+This is a vendored copy of 2 packages (`clock` and `collections`) from the
+github.com/mailgun/holster@v4.2.5 module.
+
+The `clock` package was completely copied over and the following modifications
+were made:
+
+* pkg/errors was replaced with the stdlib errors package / fmt.Errorf's %w;
+* import names changed in blackbox test packages;
+* a small race condition in the testing logic was fixed using the provided
+ mutex.
+
+The `collections` package only contains the priority_queue and ttlmap and
+corresponding test files. The only changes made to those files were to adjust
+the package names to use the vendored packages.
+
+## Why
+
+TL;DR: holster is a utility repo with many dependencies and even with graph
+pruning using it in oxy can transitively impact oxy users in negative ways by
+forcing version bumps (at the least).
+
+Full details can be found here: https://github.com/vulcand/oxy/pull/223
diff --git a/internal/holsterv4/clock/README.md b/internal/holsterv4/clock/README.md
new file mode 100644
index 0000000..5caff75
--- /dev/null
+++ b/internal/holsterv4/clock/README.md
@@ -0,0 +1,47 @@
+# Clock
+
+A drop in (almost) replacement for the system `time` package. It provides a way
+to make scheduled calls, timers and tickers deterministic in tests. By default
+it forwards all calls to the system `time` package. In test, however, it is
+possible to enable the frozen clock mode, and advance time manually to make
+scheduled even trigger at certain moments.
+
+# Usage
+
+```go
+package foo
+
+import (
+ "testing"
+
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSleep(t *testing.T) {
+ // Freeze switches the clock package to the frozen clock mode. You need to
+ // advance time manually from now on. Note that all scheduled events, timers
+ // and ticker created before this call keep operating in real time.
+ //
+ // The initial time is set to now here, but you can set any datetime.
+ clock.Freeze(clock.Now())
+ // Do not forget to revert the effect of Freeze at the end of the test.
+ defer clock.Unfreeze()
+
+ var fired bool
+
+ clock.AfterFunc(100*clock.Millisecond, func() {
+ fired = true
+ })
+ clock.Advance(93*clock.Millisecond)
+
+ // Advance will make all fire all events, timers, tickers that are
+ // scheduled for the passed period of time. Note that scheduled functions
+ // are called from within Advanced unlike system time package that calls
+ // them in their own goroutine.
+ assert.Equal(t, 97*clock.Millisecond, clock.Advance(6*clock.Millisecond))
+ assert.True(t, fired)
+ assert.Equal(t, 100*clock.Millisecond, clock.Advance(1*clock.Millisecond))
+ assert.True(t, fired)
+}
+```
diff --git a/internal/holsterv4/clock/clock.go b/internal/holsterv4/clock/clock.go
new file mode 100644
index 0000000..ca329ad
--- /dev/null
+++ b/internal/holsterv4/clock/clock.go
@@ -0,0 +1,105 @@
+//go:build !holster_test_mode
+
+// Package clock provides the same functions as the system package time. In
+// production it forwards all calls to the system time package, but in tests
+// the time can be frozen by calling Freeze function and from that point it has
+// to be advanced manually with Advance function making all scheduled calls
+// deterministic.
+//
+// The functions provided by the package have the same parameters and return
+// values as their system counterparts with a few exceptions. Where either
+// *time.Timer or *time.Ticker is returned by a system function, the clock
+// package counterpart returns clock.Timer or clock.Ticker interface
+// respectively. The interfaces provide API as respective structs except C is
+// not a channel, but a function that returns <-chan time.Time.
+package clock
+
+import "time"
+
+var (
+ frozenAt time.Time
+ realtime = &systemTime{}
+ provider Clock = realtime
+)
+
+// Freeze after this function is called all time related functions start
+// generate deterministic timers that are triggered by Advance function. It is
+// supposed to be used in tests only. Returns an Unfreezer so it can be a
+// one-liner in tests: defer clock.Freeze(clock.Now()).Unfreeze()
+func Freeze(now time.Time) Unfreezer {
+ frozenAt = now.UTC()
+ provider = &frozenTime{now: now}
+ return Unfreezer{}
+}
+
+type Unfreezer struct{}
+
+func (u Unfreezer) Unfreeze() {
+ Unfreeze()
+}
+
+// Unfreeze reverses effect of Freeze.
+func Unfreeze() {
+ provider = realtime
+}
+
+// Realtime returns a clock provider wrapping the SDK's time package. It is
+// supposed to be used in tests when time is frozen to schedule test timeouts.
+func Realtime() Clock {
+ return realtime
+}
+
+// Makes the deterministic time move forward by the specified duration, firing
+// timers along the way in the natural order. It returns how much time has
+// passed since it was frozen. So you can assert on the return value in tests
+// to make it explicit where you stand on the deterministic time scale.
+func Advance(d time.Duration) time.Duration {
+ ft, ok := provider.(*frozenTime)
+ if !ok {
+ panic("Freeze time first!")
+ }
+ ft.advance(d)
+ return Now().UTC().Sub(frozenAt)
+}
+
+// Wait4Scheduled blocks until either there are n or more scheduled events, or
+// the timeout elapses. It returns true if the wait condition has been met
+// before the timeout expired, false otherwise.
+func Wait4Scheduled(count int, timeout time.Duration) bool {
+ return provider.Wait4Scheduled(count, timeout)
+}
+
+// Now see time.Now.
+func Now() time.Time {
+ return provider.Now()
+}
+
+// Sleep see time.Sleep.
+func Sleep(d time.Duration) {
+ provider.Sleep(d)
+}
+
+// After see time.After.
+func After(d time.Duration) <-chan time.Time {
+ return provider.After(d)
+}
+
+// NewTimer see time.NewTimer.
+func NewTimer(d time.Duration) Timer {
+ return provider.NewTimer(d)
+}
+
+// AfterFunc see time.AfterFunc.
+func AfterFunc(d time.Duration, f func()) Timer {
+ return provider.AfterFunc(d, f)
+}
+
+// NewTicker see time.Ticker.
+func NewTicker(d time.Duration) Ticker {
+ return provider.NewTicker(d)
+}
+
+// Tick see time.Tick.
+func Tick(d time.Duration) <-chan time.Time {
+ return provider.Tick(d)
+}
diff --git a/internal/holsterv4/clock/clock_mutex.go b/internal/holsterv4/clock/clock_mutex.go
new file mode 100644
index 0000000..f7f8708
--- /dev/null
+++ b/internal/holsterv4/clock/clock_mutex.go
@@ -0,0 +1,131 @@
+//go:build holster_test_mode
+
+// Package clock provides the same functions as the system package time. In
+// production it forwards all calls to the system time package, but in tests
+// the time can be frozen by calling Freeze function and from that point it has
+// to be advanced manually with Advance function making all scheduled calls
+// deterministic.
+//
+// The functions provided by the package have the same parameters and return
+// values as their system counterparts with a few exceptions. Where either
+// *time.Timer or *time.Ticker is returned by a system function, the clock
+// package counterpart returns clock.Timer or clock.Ticker interface
+// respectively. The interfaces provide API as respective structs except C is
+// not a channel, but a function that returns <-chan time.Time.
+package clock
+
+import (
+ "sync"
+ "time"
+)
+
+var (
+ frozenAt time.Time
+ realtime = &systemTime{}
+ provider Clock = realtime
+ rwMutex = sync.RWMutex{}
+)
+
+// Freeze after this function is called all time related functions start
+// generate deterministic timers that are triggered by Advance function. It is
+// supposed to be used in tests only. Returns an Unfreezer so it can be a
+// one-liner in tests: defer clock.Freeze(clock.Now()).Unfreeze()
+func Freeze(now time.Time) Unfreezer {
+ frozenAt = now.UTC()
+ rwMutex.Lock()
+ defer rwMutex.Unlock()
+ provider = &frozenTime{now: now}
+ return Unfreezer{}
+}
+
+type Unfreezer struct{}
+
+func (u Unfreezer) Unfreeze() {
+ Unfreeze()
+}
+
+// Unfreeze reverses effect of Freeze.
+func Unfreeze() {
+ rwMutex.Lock()
+ defer rwMutex.Unlock()
+ provider = realtime
+}
+
+// Realtime returns a clock provider wrapping the SDK's time package. It is
+// supposed to be used in tests when time is frozen to schedule test timeouts.
+func Realtime() Clock {
+ return realtime
+}
+
+// Makes the deterministic time move forward by the specified duration, firing
+// timers along the way in the natural order. It returns how much time has
+// passed since it was frozen. So you can assert on the return value in tests
+// to make it explicit where you stand on the deterministic time scale.
+func Advance(d time.Duration) time.Duration {
+ rwMutex.RLock()
+ ft, ok := provider.(*frozenTime)
+ rwMutex.RUnlock()
+ if !ok {
+ panic("Freeze time first!")
+ }
+ ft.advance(d)
+ return Now().UTC().Sub(frozenAt)
+}
+
+// Wait4Scheduled blocks until either there are n or more scheduled events, or
+// the timeout elapses. It returns true if the wait condition has been met
+// before the timeout expired, false otherwise.
+func Wait4Scheduled(count int, timeout time.Duration) bool {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.Wait4Scheduled(count, timeout)
+}
+
+// Now see time.Now.
+func Now() time.Time {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.Now()
+}
+
+// Sleep see time.Sleep.
+func Sleep(d time.Duration) {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ provider.Sleep(d)
+}
+
+// After see time.After.
+func After(d time.Duration) <-chan time.Time {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.After(d)
+}
+
+// NewTimer see time.NewTimer.
+func NewTimer(d time.Duration) Timer {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.NewTimer(d)
+}
+
+// AfterFunc see time.AfterFunc.
+func AfterFunc(d time.Duration, f func()) Timer {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.AfterFunc(d, f)
+}
+
+// NewTicker see time.Ticker.
+func NewTicker(d time.Duration) Ticker {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.NewTicker(d)
+}
+
+// Tick see time.Tick.
+func Tick(d time.Duration) <-chan time.Time {
+ rwMutex.RLock()
+ defer rwMutex.RUnlock()
+ return provider.Tick(d)
+}
diff --git a/internal/holsterv4/clock/duration.go b/internal/holsterv4/clock/duration.go
new file mode 100644
index 0000000..f15801f
--- /dev/null
+++ b/internal/holsterv4/clock/duration.go
@@ -0,0 +1,65 @@
+package clock
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+type DurationJSON struct {
+ Duration Duration
+}
+
+func NewDurationJSON(v interface{}) (DurationJSON, error) {
+ switch v := v.(type) {
+ case Duration:
+ return DurationJSON{Duration: v}, nil
+ case float64:
+ return DurationJSON{Duration: Duration(v)}, nil
+ case int64:
+ return DurationJSON{Duration: Duration(v)}, nil
+ case int:
+ return DurationJSON{Duration: Duration(v)}, nil
+ case []byte:
+ duration, err := ParseDuration(string(v))
+ if err != nil {
+ return DurationJSON{}, fmt.Errorf("while parsing []byte: %w", err)
+ }
+ return DurationJSON{Duration: duration}, nil
+ case string:
+ duration, err := ParseDuration(v)
+ if err != nil {
+ return DurationJSON{}, fmt.Errorf("while parsing string: %w", err)
+ }
+ return DurationJSON{Duration: duration}, nil
+ default:
+ return DurationJSON{}, fmt.Errorf("bad type %T", v)
+ }
+}
+
+func NewDurationJSONOrPanic(v interface{}) DurationJSON {
+ d, err := NewDurationJSON(v)
+ if err != nil {
+ panic(err)
+ }
+ return d
+}
+
+func (d DurationJSON) MarshalJSON() ([]byte, error) {
+ return json.Marshal(d.Duration.String())
+}
+
+func (d *DurationJSON) UnmarshalJSON(b []byte) error {
+ var v interface{}
+ var err error
+
+ if err = json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+
+ *d, err = NewDurationJSON(v)
+ return err
+}
+
+func (d DurationJSON) String() string {
+ return d.Duration.String()
+}
diff --git a/internal/holsterv4/clock/duration_test.go b/internal/holsterv4/clock/duration_test.go
new file mode 100644
index 0000000..eb465ed
--- /dev/null
+++ b/internal/holsterv4/clock/duration_test.go
@@ -0,0 +1,79 @@
+package clock_test
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
+)
+
+type DurationSuite struct {
+ suite.Suite
+}
+
+func TestDurationSuite(t *testing.T) {
+ suite.Run(t, new(DurationSuite))
+}
+
+func (s *DurationSuite) TestNewOk() {
+ for _, v := range []interface{}{
+ 42 * clock.Second,
+ int(42000000000),
+ int64(42000000000),
+ 42000000000.,
+ "42s",
+ []byte("42s"),
+ } {
+ d, err := clock.NewDurationJSON(v)
+ s.Nil(err)
+ s.Equal(42*clock.Second, d.Duration)
+ }
+}
+
+func (s *DurationSuite) TestNewError() {
+ for _, tc := range []struct {
+ v interface{}
+ errMsg string
+ }{{
+ v: "foo",
+ errMsg: "while parsing string: time: invalid duration \"foo\"",
+ }, {
+ v: []byte("foo"),
+ errMsg: "while parsing []byte: time: invalid duration \"foo\"",
+ }, {
+ v: true,
+ errMsg: "bad type bool",
+ }} {
+ d, err := clock.NewDurationJSON(tc.v)
+ s.Equal(tc.errMsg, err.Error())
+ s.Equal(clock.DurationJSON{}, d)
+ }
+}
+
+func (s *DurationSuite) TestUnmarshal() {
+ for _, v := range []string{
+ `{"foo": 42000000000}`,
+ `{"foo": 0.42e11}`,
+ `{"foo": "42s"}`,
+ } {
+ var withDuration struct {
+ Foo clock.DurationJSON `json:"foo"`
+ }
+ err := json.Unmarshal([]byte(v), &withDuration)
+ s.Nil(err)
+ s.Equal(42*clock.Second, withDuration.Foo.Duration)
+ }
+}
+
+func (s *DurationSuite) TestMarshalling() {
+ d, err := clock.NewDurationJSON(42 * clock.Second)
+ s.Nil(err)
+ encoded, err := d.MarshalJSON()
+ s.Nil(err)
+ var decoded clock.DurationJSON
+ err = decoded.UnmarshalJSON(encoded)
+ s.Nil(err)
+ s.Equal(d, decoded)
+ s.Equal("42s", decoded.String())
+}
diff --git a/internal/holsterv4/clock/frozen.go b/internal/holsterv4/clock/frozen.go
new file mode 100644
index 0000000..df85d00
--- /dev/null
+++ b/internal/holsterv4/clock/frozen.go
@@ -0,0 +1,231 @@
+package clock
+
+import (
+ "errors"
+ "sync"
+ "time"
+)
+
+type frozenTime struct {
+ mu sync.Mutex
+ now time.Time
+ timers []*frozenTimer
+ waiter *waiter
+}
+
+type waiter struct {
+ count int
+ signalCh chan struct{}
+}
+
+func (ft *frozenTime) Now() time.Time {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+ return ft.now
+}
+
+func (ft *frozenTime) Sleep(d time.Duration) {
+ <-ft.NewTimer(d).C()
+}
+
+func (ft *frozenTime) After(d time.Duration) <-chan time.Time {
+ return ft.NewTimer(d).C()
+}
+
+func (ft *frozenTime) NewTimer(d time.Duration) Timer {
+ return ft.AfterFunc(d, nil)
+}
+
+func (ft *frozenTime) AfterFunc(d time.Duration, f func()) Timer {
+ t := &frozenTimer{
+ ft: ft,
+ when: ft.Now().Add(d),
+ f: f,
+ }
+ if f == nil {
+ t.c = make(chan time.Time, 1)
+ }
+ ft.startTimer(t)
+ return t
+}
+
+func (ft *frozenTime) advance(d time.Duration) {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+
+ ft.now = ft.now.Add(d)
+ for t := ft.nextExpired(); t != nil; t = ft.nextExpired() {
+ // Send the timer expiration time to the timer channel if it is
+ // defined. But make sure not to block on the send if the channel is
+ // full. This behavior will make a ticker skip beats if it readers are
+ // not fast enough.
+ if t.c != nil {
+ select {
+ case t.c <- t.when:
+ default:
+ }
+ }
+ // If it is a ticking timer then schedule next tick, otherwise mark it
+ // as stopped.
+ if t.interval != 0 {
+ t.when = t.when.Add(t.interval)
+ t.stopped = false
+ ft.unlockedStartTimer(t)
+ }
+ // If a function is associated with the timer then call it, but make
+ // sure to release the lock for the time of call it is necessary
+ // because the lock is not re-entrant but the function may need to
+ // start another timer or ticker.
+ if t.f != nil {
+ func() {
+ ft.mu.Unlock()
+ defer ft.mu.Lock()
+ t.f()
+ }()
+ }
+ }
+}
+
+func (ft *frozenTime) stopTimer(t *frozenTimer) bool {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+
+ if t.stopped {
+ return false
+ }
+ for i, curr := range ft.timers {
+ if curr == t {
+ t.stopped = true
+ copy(ft.timers[i:], ft.timers[i+1:])
+ lastIdx := len(ft.timers) - 1
+ ft.timers[lastIdx] = nil
+ ft.timers = ft.timers[:lastIdx]
+ return true
+ }
+ }
+ return false
+}
+
+func (ft *frozenTime) nextExpired() *frozenTimer {
+ if len(ft.timers) == 0 {
+ return nil
+ }
+ t := ft.timers[0]
+ if ft.now.Before(t.when) {
+ return nil
+ }
+ copy(ft.timers, ft.timers[1:])
+ lastIdx := len(ft.timers) - 1
+ ft.timers[lastIdx] = nil
+ ft.timers = ft.timers[:lastIdx]
+ t.stopped = true
+ return t
+}
+
+func (ft *frozenTime) startTimer(t *frozenTimer) {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+
+ ft.unlockedStartTimer(t)
+
+ if ft.waiter == nil {
+ return
+ }
+ if len(ft.timers) >= ft.waiter.count {
+ close(ft.waiter.signalCh)
+ }
+}
+
+func (ft *frozenTime) unlockedStartTimer(t *frozenTimer) {
+ pos := 0
+ for _, curr := range ft.timers {
+ if t.when.Before(curr.when) {
+ break
+ }
+ pos++
+ }
+ ft.timers = append(ft.timers, nil)
+ copy(ft.timers[pos+1:], ft.timers[pos:])
+ ft.timers[pos] = t
+}
+
+type frozenTimer struct {
+ ft *frozenTime
+ when time.Time
+ interval time.Duration
+ stopped bool
+ c chan time.Time
+ f func()
+}
+
+func (t *frozenTimer) C() <-chan time.Time {
+ return t.c
+}
+
+func (t *frozenTimer) Stop() bool {
+ return t.ft.stopTimer(t)
+}
+
+func (t *frozenTimer) Reset(d time.Duration) bool {
+ active := t.ft.stopTimer(t)
+ t.when = t.ft.Now().Add(d)
+ t.ft.startTimer(t)
+ return active
+}
+
+type frozenTicker struct {
+ t *frozenTimer
+}
+
+func (t *frozenTicker) C() <-chan time.Time {
+ return t.t.C()
+}
+
+func (t *frozenTicker) Stop() {
+ t.t.Stop()
+}
+
+func (ft *frozenTime) NewTicker(d time.Duration) Ticker {
+ if d <= 0 {
+ panic(errors.New("non-positive interval for NewTicker"))
+ }
+ t := &frozenTimer{
+ ft: ft,
+ when: ft.Now().Add(d),
+ interval: d,
+ c: make(chan time.Time, 1),
+ }
+ ft.startTimer(t)
+ return &frozenTicker{t}
+}
+
+func (ft *frozenTime) Tick(d time.Duration) <-chan time.Time {
+ if d <= 0 {
+ return nil
+ }
+ return ft.NewTicker(d).C()
+}
+
+func (ft *frozenTime) Wait4Scheduled(count int, timeout time.Duration) bool {
+ ft.mu.Lock()
+ if len(ft.timers) >= count {
+ ft.mu.Unlock()
+ return true
+ }
+ if ft.waiter != nil {
+ panic("Concurrent call")
+ }
+ ft.waiter = &waiter{count, make(chan struct{})}
+ ft.mu.Unlock()
+
+ success := false
+ select {
+ case <-ft.waiter.signalCh:
+ success = true
+ case <-time.After(timeout):
+ }
+ ft.mu.Lock()
+ ft.waiter = nil
+ ft.mu.Unlock()
+ return success
+}
diff --git a/internal/holsterv4/clock/frozen_test.go b/internal/holsterv4/clock/frozen_test.go
new file mode 100644
index 0000000..0e07368
--- /dev/null
+++ b/internal/holsterv4/clock/frozen_test.go
@@ -0,0 +1,340 @@
+package clock
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+)
+
+func TestFreezeUnfreeze(t *testing.T) {
+ defer Freeze(Now()).Unfreeze()
+}
+
+type FrozenSuite struct {
+ suite.Suite
+ epoch time.Time
+}
+
+func TestFrozenSuite(t *testing.T) {
+ suite.Run(t, new(FrozenSuite))
+}
+
+func (s *FrozenSuite) SetupSuite() {
+ var err error
+ s.epoch, err = time.Parse(time.RFC3339, "2009-02-19T00:00:00Z")
+ s.Require().NoError(err)
+}
+
+func (s *FrozenSuite) SetupTest() {
+ Freeze(s.epoch)
+}
+
+func (s *FrozenSuite) TearDownTest() {
+ Unfreeze()
+}
+
+func (s *FrozenSuite) TestAdvanceNow() {
+ s.Require().Equal(s.epoch, Now())
+ s.Require().Equal(42*time.Millisecond, Advance(42*time.Millisecond))
+ s.Require().Equal(s.epoch.Add(42*time.Millisecond), Now())
+ s.Require().Equal(55*time.Millisecond, Advance(13*time.Millisecond))
+ s.Require().Equal(74*time.Millisecond, Advance(19*time.Millisecond))
+ s.Require().Equal(s.epoch.Add(74*time.Millisecond), Now())
+}
+
+func (s *FrozenSuite) TestSleep() {
+ hits := make(chan int, 100)
+
+ delays := []int{60, 100, 90, 131, 999, 5}
+ for i, tc := range []struct {
+ desc string
+ fn func(delayMs int)
+ }{{
+ desc: "Sleep",
+ fn: func(delay int) {
+ Sleep(time.Duration(delay) * time.Millisecond)
+ hits <- delay
+ },
+ }, {
+ desc: "After",
+ fn: func(delay int) {
+ <-After(time.Duration(delay) * time.Millisecond)
+ hits <- delay
+ },
+ }, {
+ desc: "AfterFunc",
+ fn: func(delay int) {
+ AfterFunc(time.Duration(delay)*time.Millisecond,
+ func() {
+ hits <- delay
+ })
+ },
+ }, {
+ desc: "NewTimer",
+ fn: func(delay int) {
+ t := NewTimer(time.Duration(delay) * time.Millisecond)
+ <-t.C()
+ hits <- delay
+ },
+ }} {
+ fmt.Printf("Test case #%d: %s", i, tc.desc)
+ for _, delay := range delays {
+ go tc.fn(delay)
+ }
+ // Spin-wait for all goroutines to fall asleep.
+ ft := provider.(*frozenTime)
+ for {
+ var brk bool
+ ft.mu.Lock()
+ if len(ft.timers) == len(delays) {
+ brk = true
+ }
+ ft.mu.Unlock()
+ if brk {
+ break
+ }
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ runningMs := 0
+ for i, delayMs := range []int{5, 60, 90, 100, 131, 999} {
+ fmt.Printf("Checking timer #%d, delay=%d\n", i, delayMs)
+ delta := delayMs - runningMs - 1
+ Advance(time.Duration(delta) * time.Millisecond)
+ // Check before each timer deadline that it is not triggered yet.
+ s.assertHits(hits, []int{})
+
+ // When
+ Advance(1 * time.Millisecond)
+
+ // Then
+ s.assertHits(hits, []int{delayMs})
+
+ runningMs += delta + 1
+ }
+
+ Advance(1000 * time.Millisecond)
+ s.assertHits(hits, []int{})
+ }
+}
+
+// Timers scheduled to trigger at the same time do that in the order they were
+// created.
+func (s *FrozenSuite) TestSameTime() {
+ var hits []int
+
+ AfterFunc(100, func() { hits = append(hits, 3) })
+ AfterFunc(100, func() { hits = append(hits, 1) })
+ AfterFunc(99, func() { hits = append(hits, 2) })
+ AfterFunc(100, func() { hits = append(hits, 5) })
+ AfterFunc(101, func() { hits = append(hits, 4) })
+ AfterFunc(101, func() { hits = append(hits, 6) })
+
+ // When
+ Advance(100)
+
+ // Then
+ s.Require().Equal([]int{2, 3, 1, 5}, hits)
+}
+
+func (s *FrozenSuite) TestTimerStop() {
+ hits := []int{}
+
+ AfterFunc(100, func() { hits = append(hits, 1) })
+ t := AfterFunc(100, func() { hits = append(hits, 2) })
+ AfterFunc(100, func() { hits = append(hits, 3) })
+ Advance(99)
+ s.Require().Equal([]int{}, hits)
+
+ // When
+ active1 := t.Stop()
+ active2 := t.Stop()
+
+ // Then
+ s.Require().Equal(true, active1)
+ s.Require().Equal(false, active2)
+ Advance(1)
+ s.Require().Equal([]int{1, 3}, hits)
+}
+
+func (s *FrozenSuite) TestReset() {
+ hits := []int{}
+
+ t1 := AfterFunc(100, func() { hits = append(hits, 1) })
+ t2 := AfterFunc(100, func() { hits = append(hits, 2) })
+ AfterFunc(100, func() { hits = append(hits, 3) })
+ Advance(99)
+ s.Require().Equal([]int{}, hits)
+
+ // When
+ active1 := t1.Reset(1) // Reset to the same time
+ active2 := t2.Reset(7)
+
+ // Then
+ s.Require().Equal(true, active1)
+ s.Require().Equal(true, active2)
+
+ Advance(1)
+ s.Require().Equal([]int{3, 1}, hits)
+ Advance(5)
+ s.Require().Equal([]int{3, 1}, hits)
+ Advance(1)
+ s.Require().Equal([]int{3, 1, 2}, hits)
+}
+
+// Reset to the same time just puts the timer at the end of the trigger list
+// for the date.
+func (s *FrozenSuite) TestResetSame() {
+ hits := []int{}
+
+ t := AfterFunc(100, func() { hits = append(hits, 1) })
+ AfterFunc(100, func() { hits = append(hits, 2) })
+ AfterFunc(100, func() { hits = append(hits, 3) })
+ AfterFunc(101, func() { hits = append(hits, 4) })
+ Advance(9)
+
+ // When
+ active := t.Reset(91)
+
+ // Then
+ s.Require().Equal(true, active)
+
+ Advance(90)
+ s.Require().Equal([]int{}, hits)
+ Advance(1)
+ s.Require().Equal([]int{2, 3, 1}, hits)
+}
+
+func (s *FrozenSuite) TestTicker() {
+ t := NewTicker(100)
+
+ Advance(99)
+ s.assertNotFired(t.C())
+ Advance(1)
+ s.Require().Equal(<-t.C(), s.epoch.Add(100))
+ Advance(750)
+ s.Require().Equal(<-t.C(), s.epoch.Add(200))
+ Advance(49)
+ s.assertNotFired(t.C())
+ Advance(1)
+ s.Require().Equal(<-t.C(), s.epoch.Add(900))
+
+ t.Stop()
+ Advance(300)
+ s.assertNotFired(t.C())
+}
+
+func (s *FrozenSuite) TestTickerZero() {
+ defer func() {
+ recover()
+ }()
+
+ NewTicker(0)
+ s.Fail("Should panic")
+}
+
+func (s *FrozenSuite) TestTick() {
+ ch := Tick(100)
+
+ Advance(99)
+ s.assertNotFired(ch)
+ Advance(1)
+ s.Require().Equal(<-ch, s.epoch.Add(100))
+ Advance(750)
+ s.Require().Equal(<-ch, s.epoch.Add(200))
+ Advance(49)
+ s.assertNotFired(ch)
+ Advance(1)
+ s.Require().Equal(<-ch, s.epoch.Add(900))
+}
+
+func (s *FrozenSuite) TestTickZero() {
+ ch := Tick(0)
+ s.Require().Nil(ch)
+}
+
+func (s *FrozenSuite) TestNewStoppedTimer() {
+ t := NewStoppedTimer()
+
+ // When/Then
+ select {
+ case <-t.C():
+ s.Fail("Timer should not have fired")
+ default:
+ }
+ s.Require().Equal(false, t.Stop())
+}
+
+func (s *FrozenSuite) TestWait4Scheduled() {
+ After(100 * Millisecond)
+ After(100 * Millisecond)
+ s.Require().Equal(false, Wait4Scheduled(3, 0))
+
+ startedCh := make(chan struct{})
+ resultCh := make(chan bool)
+ go func() {
+ close(startedCh)
+ resultCh <- Wait4Scheduled(3, 5*Second)
+ }()
+ // Allow some time for waiter to be set and start waiting for a signal.
+ <-startedCh
+ time.Sleep(50 * Millisecond)
+
+ // When
+ After(100 * Millisecond)
+
+ // Then
+ s.Require().Equal(true, <-resultCh)
+}
+
+// If there is enough timers scheduled already, then a shortcut execution path
+// is taken and Wait4Scheduled returns immediately.
+func (s *FrozenSuite) TestWait4ScheduledImmediate() {
+ After(100 * Millisecond)
+ After(100 * Millisecond)
+ // When/Then
+ s.Require().Equal(true, Wait4Scheduled(2, 0))
+}
+
+func (s *FrozenSuite) TestSince() {
+ s.Require().Equal(Duration(0), Since(Now()))
+ s.Require().Equal(-Millisecond, Since(Now().Add(Millisecond)))
+ s.Require().Equal(Millisecond, Since(Now().Add(-Millisecond)))
+}
+
+func (s *FrozenSuite) TestUntil() {
+ s.Require().Equal(Duration(0), Until(Now()))
+ s.Require().Equal(Millisecond, Until(Now().Add(Millisecond)))
+ s.Require().Equal(-Millisecond, Until(Now().Add(-Millisecond)))
+}
+
+func (s *FrozenSuite) assertHits(got <-chan int, want []int) {
+ for i, w := range want {
+ var g int
+ select {
+ case g = <-got:
+ case <-time.After(100 * time.Millisecond):
+ s.Failf("Missing hit", "want=%v", w)
+ return
+ }
+ s.Require().Equal(w, g, "Hit #%d", i)
+ }
+ for {
+ select {
+ case g := <-got:
+ s.Failf("Unexpected hit", "got=%v", g)
+ default:
+ return
+ }
+ }
+}
+
+func (s *FrozenSuite) assertNotFired(ch <-chan time.Time) {
+ select {
+ case <-ch:
+ s.Fail("Premature fire")
+ default:
+ }
+}
diff --git a/internal/holsterv4/clock/go19.go b/internal/holsterv4/clock/go19.go
new file mode 100644
index 0000000..f5e169e
--- /dev/null
+++ b/internal/holsterv4/clock/go19.go
@@ -0,0 +1,106 @@
+// +build go1.9
+
+// This file introduces aliases to allow using of the clock package as a
+// drop-in replacement of the standard time package.
+
+package clock
+
+import "time"
+
+type (
+ Time = time.Time
+ Duration = time.Duration
+ Location = time.Location
+
+ Weekday = time.Weekday
+ Month = time.Month
+
+ ParseError = time.ParseError
+)
+
+const (
+ Nanosecond = time.Nanosecond
+ Microsecond = time.Microsecond
+ Millisecond = time.Millisecond
+ Second = time.Second
+ Minute = time.Minute
+ Hour = time.Hour
+
+ Sunday = time.Sunday
+ Monday = time.Monday
+ Tuesday = time.Tuesday
+ Wednesday = time.Wednesday
+ Thursday = time.Thursday
+ Friday = time.Friday
+ Saturday = time.Saturday
+
+ January = time.January
+ February = time.February
+ March = time.March
+ April = time.April
+ May = time.May
+ June = time.June
+ July = time.July
+ August = time.August
+ September = time.September
+ October = time.October
+ November = time.November
+ December = time.December
+
+ ANSIC = time.ANSIC
+ UnixDate = time.UnixDate
+ RubyDate = time.RubyDate
+ RFC822 = time.RFC822
+ RFC822Z = time.RFC822Z
+ RFC850 = time.RFC850
+ RFC1123 = time.RFC1123
+ RFC1123Z = time.RFC1123Z
+ RFC3339 = time.RFC3339
+ RFC3339Nano = time.RFC3339Nano
+ Kitchen = time.Kitchen
+ Stamp = time.Stamp
+ StampMilli = time.StampMilli
+ StampMicro = time.StampMicro
+ StampNano = time.StampNano
+)
+
+var (
+ UTC = time.UTC
+ Local = time.Local
+)
+
+func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {
+ return time.Date(year, month, day, hour, min, sec, nsec, loc)
+}
+
+func FixedZone(name string, offset int) *Location {
+ return time.FixedZone(name, offset)
+}
+
+func LoadLocation(name string) (*Location, error) {
+ return time.LoadLocation(name)
+}
+
+func Parse(layout, value string) (Time, error) {
+ return time.Parse(layout, value)
+}
+
+func ParseDuration(s string) (Duration, error) {
+ return time.ParseDuration(s)
+}
+
+func ParseInLocation(layout, value string, loc *Location) (Time, error) {
+ return time.ParseInLocation(layout, value, loc)
+}
+
+func Unix(sec int64, nsec int64) Time {
+ return time.Unix(sec, nsec)
+}
+
+func Since(t Time) Duration {
+ return provider.Now().Sub(t)
+}
+
+func Until(t Time) Duration {
+ return t.Sub(provider.Now())
+}
diff --git a/internal/holsterv4/clock/interface.go b/internal/holsterv4/clock/interface.go
new file mode 100644
index 0000000..15f5ca1
--- /dev/null
+++ b/internal/holsterv4/clock/interface.go
@@ -0,0 +1,35 @@
+package clock
+
+import "time"
+
+// Timer see time.Timer.
+type Timer interface {
+ C() <-chan time.Time
+ Stop() bool
+ Reset(d time.Duration) bool
+}
+
+// Ticker see time.Ticker.
+type Ticker interface {
+ C() <-chan time.Time
+ Stop()
+}
+
+// NewStoppedTimer returns a stopped timer. Call Reset to get it ticking.
+func NewStoppedTimer() Timer {
+ t := NewTimer(42 * time.Hour)
+ t.Stop()
+ return t
+}
+
+// Clock is an interface that mimics the one of the SDK time package.
+type Clock interface {
+ Now() time.Time
+ Sleep(d time.Duration)
+ After(d time.Duration) <-chan time.Time
+ NewTimer(d time.Duration) Timer
+ AfterFunc(d time.Duration, f func()) Timer
+ NewTicker(d time.Duration) Ticker
+ Tick(d time.Duration) <-chan time.Time
+ Wait4Scheduled(n int, timeout time.Duration) bool
+}
diff --git a/internal/holsterv4/clock/rfc822.go b/internal/holsterv4/clock/rfc822.go
new file mode 100644
index 0000000..664941d
--- /dev/null
+++ b/internal/holsterv4/clock/rfc822.go
@@ -0,0 +1,119 @@
+package clock
+
+import (
+ "strconv"
+ "time"
+)
+
+var datetimeLayouts = [48]string{
+ // Day first month 2nd abbreviated.
+ "Mon, 2 Jan 2006 15:04:05 MST",
+ "Mon, 2 Jan 2006 15:04:05 -0700",
+ "Mon, 2 Jan 2006 15:04:05 -0700 (MST)",
+ "2 Jan 2006 15:04:05 MST",
+ "2 Jan 2006 15:04:05 -0700",
+ "2 Jan 2006 15:04:05 -0700 (MST)",
+ "Mon, 2 Jan 2006 15:04 MST",
+ "Mon, 2 Jan 2006 15:04 -0700",
+ "Mon, 2 Jan 2006 15:04 -0700 (MST)",
+ "2 Jan 2006 15:04 MST",
+ "2 Jan 2006 15:04 -0700",
+ "2 Jan 2006 15:04 -0700 (MST)",
+
+ // Month first day 2nd abbreviated.
+ "Mon, Jan 2 2006 15:04:05 MST",
+ "Mon, Jan 2 2006 15:04:05 -0700",
+ "Mon, Jan 2 2006 15:04:05 -0700 (MST)",
+ "Jan 2 2006 15:04:05 MST",
+ "Jan 2 2006 15:04:05 -0700",
+ "Jan 2 2006 15:04:05 -0700 (MST)",
+ "Mon, Jan 2 2006 15:04 MST",
+ "Mon, Jan 2 2006 15:04 -0700",
+ "Mon, Jan 2 2006 15:04 -0700 (MST)",
+ "Jan 2 2006 15:04 MST",
+ "Jan 2 2006 15:04 -0700",
+ "Jan 2 2006 15:04 -0700 (MST)",
+
+ // Day first month 2nd not abbreviated.
+ "Mon, 2 January 2006 15:04:05 MST",
+ "Mon, 2 January 2006 15:04:05 -0700",
+ "Mon, 2 January 2006 15:04:05 -0700 (MST)",
+ "2 January 2006 15:04:05 MST",
+ "2 January 2006 15:04:05 -0700",
+ "2 January 2006 15:04:05 -0700 (MST)",
+ "Mon, 2 January 2006 15:04 MST",
+ "Mon, 2 January 2006 15:04 -0700",
+ "Mon, 2 January 2006 15:04 -0700 (MST)",
+ "2 January 2006 15:04 MST",
+ "2 January 2006 15:04 -0700",
+ "2 January 2006 15:04 -0700 (MST)",
+
+ // Month first day 2nd not abbreviated.
+ "Mon, January 2 2006 15:04:05 MST",
+ "Mon, January 2 2006 15:04:05 -0700",
+ "Mon, January 2 2006 15:04:05 -0700 (MST)",
+ "January 2 2006 15:04:05 MST",
+ "January 2 2006 15:04:05 -0700",
+ "January 2 2006 15:04:05 -0700 (MST)",
+ "Mon, January 2 2006 15:04 MST",
+ "Mon, January 2 2006 15:04 -0700",
+ "Mon, January 2 2006 15:04 -0700 (MST)",
+ "January 2 2006 15:04 MST",
+ "January 2 2006 15:04 -0700",
+ "January 2 2006 15:04 -0700 (MST)",
+}
+
+// Allows seamless JSON encoding/decoding of rfc822 formatted timestamps.
+// https://www.ietf.org/rfc/rfc822.txt section 5.
+type RFC822Time struct {
+ Time
+}
+
+// NewRFC822Time creates RFC822Time from a standard Time. The created value is
+// truncated down to second precision because RFC822 does not allow for better.
+func NewRFC822Time(t Time) RFC822Time {
+ return RFC822Time{Time: t.Truncate(Second)}
+}
+
+// ParseRFC822Time parses an RFC822 time string.
+func ParseRFC822Time(s string) (Time, error) {
+ var t time.Time
+ var err error
+ for _, layout := range datetimeLayouts {
+ t, err = Parse(layout, s)
+ if err == nil {
+ return t, err
+ }
+ }
+ return t, err
+}
+
+// NewRFC822Time creates RFC822Time from a Unix timestamp (seconds from Epoch).
+func NewRFC822TimeFromUnix(timestamp int64) RFC822Time {
+ return RFC822Time{Time: Unix(timestamp, 0).UTC()}
+}
+
+func (t RFC822Time) MarshalJSON() ([]byte, error) {
+ return []byte(strconv.Quote(t.Format(RFC1123))), nil
+}
+
+func (t *RFC822Time) UnmarshalJSON(s []byte) error {
+ q, err := strconv.Unquote(string(s))
+ if err != nil {
+ return err
+ }
+ parsed, err := ParseRFC822Time(q)
+ if err != nil {
+ return err
+ }
+ t.Time = parsed
+ return nil
+}
+
+func (t RFC822Time) String() string {
+ return t.Format(RFC1123)
+}
+
+func (t RFC822Time) StringWithOffset() string {
+ return t.Format(RFC1123Z)
+}
diff --git a/internal/holsterv4/clock/rfc822_test.go b/internal/holsterv4/clock/rfc822_test.go
new file mode 100644
index 0000000..d83d6bf
--- /dev/null
+++ b/internal/holsterv4/clock/rfc822_test.go
@@ -0,0 +1,205 @@
+package clock
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type testStruct struct {
+ Time RFC822Time `json:"ts"`
+}
+
+func TestRFC822New(t *testing.T) {
+ stdTime, err := Parse(RFC3339, "2019-08-29T11:20:07.123456+03:00")
+ assert.NoError(t, err)
+
+ rfc822TimeFromTime := NewRFC822Time(stdTime)
+ rfc822TimeFromUnix := NewRFC822TimeFromUnix(stdTime.Unix())
+ assert.True(t, rfc822TimeFromTime.Equal(rfc822TimeFromUnix.Time),
+ "want=%s, got=%s", rfc822TimeFromTime.Time, rfc822TimeFromUnix.Time)
+
+ // Parsing from numerical offset to abbreviated offset is not always reliable. In this
+ // context Go will fallback to the known numerical offset.
+ assert.Equal(t, "Thu, 29 Aug 2019 11:20:07 +0300", rfc822TimeFromTime.String())
+ assert.Equal(t, "Thu, 29 Aug 2019 08:20:07 UTC", rfc822TimeFromUnix.String())
+}
+
+// NewRFC822Time truncates to second precision.
+func TestRFC822SecondPrecision(t *testing.T) {
+ stdTime1, err := Parse(RFC3339, "2019-08-29T11:20:07.111111+03:00")
+ assert.NoError(t, err)
+ stdTime2, err := Parse(RFC3339, "2019-08-29T11:20:07.999999+03:00")
+ assert.NoError(t, err)
+ assert.False(t, stdTime1.Equal(stdTime2))
+
+ rfc822Time1 := NewRFC822Time(stdTime1)
+ rfc822Time2 := NewRFC822Time(stdTime2)
+ assert.True(t, rfc822Time1.Equal(rfc822Time2.Time),
+ "want=%s, got=%s", rfc822Time1.Time, rfc822Time2.Time)
+}
+
+// Marshaled representation is truncated down to second precision.
+func TestRFC822Marshaling(t *testing.T) {
+ stdTime, err := Parse(RFC3339Nano, "2019-08-29T11:20:07.123456789+03:30")
+ assert.NoError(t, err)
+
+ ts := testStruct{Time: NewRFC822Time(stdTime)}
+ encoded, err := json.Marshal(&ts)
+ assert.NoError(t, err)
+ assert.Equal(t, `{"ts":"Thu, 29 Aug 2019 11:20:07 +0330"}`, string(encoded))
+}
+
+func TestRFC822Unmarshaling(t *testing.T) {
+ for i, tc := range []struct {
+ inRFC822 string
+ outRFC3339 string
+ outRFC822 string
+ }{{
+ inRFC822: "Thu, 29 Aug 2019 11:20:07 GMT",
+ outRFC3339: "2019-08-29T11:20:07Z",
+ outRFC822: "Thu, 29 Aug 2019 11:20:07 GMT",
+ }, {
+ inRFC822: "Thu, 29 Aug 2019 11:20:07 MSK",
+ // Extrapolating the numerical offset from an abbreviated offset is unreliable. In
+ // this test case the RFC3339 will have the incorrect result due to limitation in
+ // Go's time parser.
+ outRFC3339: "2019-08-29T11:20:07Z",
+ outRFC822: "Thu, 29 Aug 2019 11:20:07 MSK",
+ }, {
+ inRFC822: "Thu, 29 Aug 2019 11:20:07 -0000",
+ outRFC3339: "2019-08-29T11:20:07Z",
+ outRFC822: "Thu, 29 Aug 2019 11:20:07 -0000",
+ }, {
+ inRFC822: "Thu, 29 Aug 2019 11:20:07 +0000",
+ outRFC3339: "2019-08-29T11:20:07Z",
+ outRFC822: "Thu, 29 Aug 2019 11:20:07 +0000",
+ }, {
+ inRFC822: "Thu, 29 Aug 2019 11:20:07 +0300",
+ outRFC3339: "2019-08-29T11:20:07+03:00",
+ outRFC822: "Thu, 29 Aug 2019 11:20:07 +0300",
+ }, {
+ inRFC822: "Thu, 29 Aug 2019 11:20:07 +0330",
+ outRFC3339: "2019-08-29T11:20:07+03:30",
+ outRFC822: "Thu, 29 Aug 2019 11:20:07 +0330",
+ }, {
+ inRFC822: "Sun, 01 Sep 2019 11:20:07 +0300",
+ outRFC3339: "2019-09-01T11:20:07+03:00",
+ outRFC822: "Sun, 01 Sep 2019 11:20:07 +0300",
+ }, {
+ inRFC822: "Sun, 1 Sep 2019 11:20:07 +0300",
+ outRFC3339: "2019-09-01T11:20:07+03:00",
+ outRFC822: "Sun, 01 Sep 2019 11:20:07 +0300",
+ }, {
+ inRFC822: "Sun, 1 Sep 2019 11:20:07 +0300",
+ outRFC3339: "2019-09-01T11:20:07+03:00",
+ outRFC822: "Sun, 01 Sep 2019 11:20:07 +0300",
+ }, {
+ inRFC822: "Sun, 1 Sep 2019 11:20:07 UTC",
+ outRFC3339: "2019-09-01T11:20:07Z",
+ outRFC822: "Sun, 01 Sep 2019 11:20:07 UTC",
+ }, {
+ inRFC822: "Sun, 1 Sep 2019 11:20:07 UTC",
+ outRFC3339: "2019-09-01T11:20:07Z",
+ outRFC822: "Sun, 01 Sep 2019 11:20:07 UTC",
+ }, {
+ inRFC822: "Sun, 1 Sep 2019 11:20:07 GMT",
+ outRFC3339: "2019-09-01T11:20:07Z",
+ outRFC822: "Sun, 01 Sep 2019 11:20:07 GMT",
+ }, {
+ inRFC822: "Fri, 21 Nov 1997 09:55:06 -0600 (MDT)",
+ outRFC3339: "1997-11-21T09:55:06-06:00",
+ outRFC822: "Fri, 21 Nov 1997 09:55:06 MDT",
+ }} {
+ t.Run(tc.inRFC822, func(t *testing.T) {
+ tcDesc := fmt.Sprintf("Test case #%d: %v", i, tc)
+ var ts testStruct
+
+ inEncoded := []byte(fmt.Sprintf(`{"ts":"%s"}`, tc.inRFC822))
+ err := json.Unmarshal(inEncoded, &ts)
+ assert.NoError(t, err, tcDesc)
+ assert.Equal(t, tc.outRFC3339, ts.Time.Format(RFC3339), tcDesc)
+
+ actualEncoded, err := json.Marshal(&ts)
+ assert.NoError(t, err, tcDesc)
+ outEncoded := fmt.Sprintf(`{"ts":"%s"}`, tc.outRFC822)
+ assert.Equal(t, outEncoded, string(actualEncoded), tcDesc)
+ })
+ }
+}
+
+func TestRFC822UnmarshalingError(t *testing.T) {
+ for _, tc := range []struct {
+ inEncoded string
+ outError string
+ }{{
+ inEncoded: `{"ts": "Thu, 29 Aug 2019 11:20:07"}`,
+ outError: `parsing time "Thu, 29 Aug 2019 11:20:07" as "January 2 2006 15:04 -0700 (MST)": cannot parse "Thu, 29 Aug 2019 11:20:07" as "January"`,
+ }, {
+ inEncoded: `{"ts": "foo"}`,
+ outError: `parsing time "foo" as "January 2 2006 15:04 -0700 (MST)": cannot parse "foo" as "January"`,
+ }, {
+ inEncoded: `{"ts": 42}`,
+ outError: "invalid syntax",
+ }} {
+ t.Run(tc.inEncoded, func(t *testing.T) {
+ var ts testStruct
+ err := json.Unmarshal([]byte(tc.inEncoded), &ts)
+ assert.EqualError(t, err, tc.outError)
+ })
+ }
+}
+
+func TestParseRFC822Time(t *testing.T) {
+ for _, tt := range []struct {
+ rfc822Time string
+ }{
+ {"Thu, 3 Jun 2021 12:01:05 MST"},
+ {"Thu, 3 Jun 2021 12:01:05 -0700"},
+ {"Thu, 3 Jun 2021 12:01:05 -0700 (MST)"},
+ {"2 Jun 2021 17:06:41 GMT"},
+ {"2 Jun 2021 17:06:41 -0700"},
+ {"2 Jun 2021 17:06:41 -0700 (MST)"},
+ {"Mon, 30 August 2021 11:05:00 -0400"},
+ {"Thu, 3 June 2021 12:01:05 MST"},
+ {"Thu, 3 June 2021 12:01:05 -0700"},
+ {"Thu, 3 June 2021 12:01:05 -0700 (MST)"},
+ {"2 June 2021 17:06:41 GMT"},
+ {"2 June 2021 17:06:41 -0700"},
+ {"2 June 2021 17:06:41 -0700 (MST)"},
+ {"Wed, Nov 03 2021 17:48:06 CST"},
+ {"Wed, November 03 2021 17:48:06 CST"},
+
+ // Timestamps without seconds.
+ {"Sun, 31 Oct 2021 12:10 -5000"},
+ {"Thu, 3 Jun 2021 12:01 MST"},
+ {"Thu, 3 Jun 2021 12:01 -0700"},
+ {"Thu, 3 Jun 2021 12:01 -0700 (MST)"},
+ {"2 Jun 2021 17:06 GMT"},
+ {"2 Jun 2021 17:06 -0700"},
+ {"2 Jun 2021 17:06 -0700 (MST)"},
+ {"Mon, 30 August 2021 11:05 -0400"},
+ {"Thu, 3 June 2021 12:01 MST"},
+ {"Thu, 3 June 2021 12:01 -0700"},
+ {"Thu, 3 June 2021 12:01 -0700 (MST)"},
+ {"2 June 2021 17:06 GMT"},
+ {"2 June 2021 17:06 -0700"},
+ {"2 June 2021 17:06 -0700 (MST)"},
+ {"Wed, Nov 03 2021 17:48 CST"},
+ {"Wed, November 03 2021 17:48 CST"},
+ } {
+ t.Run(tt.rfc822Time, func(t *testing.T) {
+ _, err := ParseRFC822Time(tt.rfc822Time)
+ assert.NoError(t, err)
+ })
+ }
+}
+
+func TestStringWithOffset(t *testing.T) {
+ now := time.Now().UTC()
+ r := NewRFC822Time(now)
+ assert.Equal(t, now.Format(time.RFC1123Z), r.StringWithOffset())
+}
diff --git a/internal/holsterv4/clock/system.go b/internal/holsterv4/clock/system.go
new file mode 100644
index 0000000..04d6673
--- /dev/null
+++ b/internal/holsterv4/clock/system.go
@@ -0,0 +1,68 @@
+package clock
+
+import "time"
+
+type systemTime struct{}
+
+func (st *systemTime) Now() time.Time {
+ return time.Now()
+}
+
+func (st *systemTime) Sleep(d time.Duration) {
+ time.Sleep(d)
+}
+
+func (st *systemTime) After(d time.Duration) <-chan time.Time {
+ return time.After(d)
+}
+
+type systemTimer struct {
+ t *time.Timer
+}
+
+func (st *systemTime) NewTimer(d time.Duration) Timer {
+ t := time.NewTimer(d)
+ return &systemTimer{t}
+}
+
+func (st *systemTime) AfterFunc(d time.Duration, f func()) Timer {
+ t := time.AfterFunc(d, f)
+ return &systemTimer{t}
+}
+
+func (t *systemTimer) C() <-chan time.Time {
+ return t.t.C
+}
+
+func (t *systemTimer) Stop() bool {
+ return t.t.Stop()
+}
+
+func (t *systemTimer) Reset(d time.Duration) bool {
+ return t.t.Reset(d)
+}
+
+type systemTicker struct {
+ t *time.Ticker
+}
+
+func (t *systemTicker) C() <-chan time.Time {
+ return t.t.C
+}
+
+func (t *systemTicker) Stop() {
+ t.t.Stop()
+}
+
+func (st *systemTime) NewTicker(d time.Duration) Ticker {
+ t := time.NewTicker(d)
+ return &systemTicker{t}
+}
+
+func (st *systemTime) Tick(d time.Duration) <-chan time.Time {
+ return time.Tick(d)
+}
+
+func (st *systemTime) Wait4Scheduled(count int, timeout time.Duration) bool {
+ panic("Not supported")
+}
diff --git a/internal/holsterv4/clock/system_test.go b/internal/holsterv4/clock/system_test.go
new file mode 100644
index 0000000..fe8345f
--- /dev/null
+++ b/internal/holsterv4/clock/system_test.go
@@ -0,0 +1,144 @@
+package clock
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSleep(t *testing.T) {
+ start := Now()
+
+ // When
+ Sleep(100 * time.Millisecond)
+
+ // Then
+ if Now().Sub(start) < 100*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+}
+
+func TestAfter(t *testing.T) {
+ start := Now()
+
+ // When
+ end := <-After(100 * time.Millisecond)
+
+ // Then
+ if end.Sub(start) < 100*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+}
+
+func TestAfterFunc(t *testing.T) {
+ start := Now()
+ endCh := make(chan time.Time, 1)
+
+ // When
+ AfterFunc(100*time.Millisecond, func() { endCh <- time.Now() })
+
+ // Then
+ end := <-endCh
+ if end.Sub(start) < 100*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+}
+
+func TestNewTimer(t *testing.T) {
+ start := Now()
+
+ // When
+ timer := NewTimer(100 * time.Millisecond)
+
+ // Then
+ end := <-timer.C()
+ if end.Sub(start) < 100*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+}
+
+func TestTimerStop(t *testing.T) {
+ timer := NewTimer(50 * time.Millisecond)
+
+ // When
+ active := timer.Stop()
+
+ // Then
+ assert.Equal(t, true, active)
+ time.Sleep(100)
+ select {
+ case <-timer.C():
+ assert.Fail(t, "Timer should not have fired")
+ default:
+ }
+}
+
+func TestTimerReset(t *testing.T) {
+ t.Skip("fail on the CI for darwin")
+ start := time.Now()
+ timer := NewTimer(300 * time.Millisecond)
+
+ // When
+ timer.Reset(100 * time.Millisecond)
+
+ // Then
+ end := <-timer.C()
+ if end.Sub(start) >= 150*time.Millisecond {
+ assert.Fail(t, "Waited too long", end.Sub(start).String())
+ }
+}
+
+func TestNewTicker(t *testing.T) {
+ start := Now()
+
+ // When
+ timer := NewTicker(100 * time.Millisecond)
+
+ // Then
+ end := <-timer.C()
+ if end.Sub(start) < 100*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+ end = <-timer.C()
+ if end.Sub(start) < 200*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+
+ timer.Stop()
+ time.Sleep(150)
+ select {
+ case <-timer.C():
+ assert.Fail(t, "Ticker should not have fired")
+ default:
+ }
+}
+
+func TestTick(t *testing.T) {
+ start := Now()
+
+ // When
+ ch := Tick(100 * time.Millisecond)
+
+ // Then
+ end := <-ch
+ if end.Sub(start) < 100*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+ end = <-ch
+ if end.Sub(start) < 200*time.Millisecond {
+ assert.Fail(t, "Sleep did not last long enough")
+ }
+}
+
+func TestNewStoppedTimer(t *testing.T) {
+ timer := NewStoppedTimer()
+
+ // When/Then
+ select {
+ case <-timer.C():
+ assert.Fail(t, "Timer should not have fired")
+ default:
+ }
+ assert.Equal(t, false, timer.Stop())
+}
diff --git a/internal/holsterv4/collections/README.md b/internal/holsterv4/collections/README.md
new file mode 100644
index 0000000..fc56c81
--- /dev/null
+++ b/internal/holsterv4/collections/README.md
@@ -0,0 +1,28 @@
+## Priority Queue
+Provides a Priority Queue implementation as described [here](https://en.wikipedia.org/wiki/Priority_queue)
+
+```go
+queue := collections.NewPriorityQueue()
+
+queue.Push(&collections.PQItem{
+ Value: "thing3",
+ Priority: 3,
+})
+
+queue.Push(&collections.PQItem{
+ Value: "thing1",
+ Priority: 1,
+})
+
+queue.Push(&collections.PQItem{
+ Value: "thing2",
+ Priority: 2,
+})
+
+// Pops item off the queue according to the priority instead of the Push() order
+item := queue.Pop()
+
+fmt.Printf("Item: %s", item.Value.(string))
+
+// Output: Item: thing1
+```
diff --git a/internal/holsterv4/collections/priority_queue.go b/internal/holsterv4/collections/priority_queue.go
new file mode 100644
index 0000000..5992ff0
--- /dev/null
+++ b/internal/holsterv4/collections/priority_queue.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2017 Mailgun Technologies Inc
+
+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 collections
+
+import (
+ "container/heap"
+)
+
+// An PQItem is something we manage in a priority queue.
+type PQItem struct {
+ Value interface{}
+ Priority int // The priority of the item in the queue.
+ // The index is needed by update and is maintained by the heap.Interface methods.
+ index int // The index of the item in the heap.
+}
+
+// Implements a PriorityQueue
+type PriorityQueue struct {
+ impl *pqImpl
+}
+
+func NewPriorityQueue() *PriorityQueue {
+ mh := &pqImpl{}
+ heap.Init(mh)
+ return &PriorityQueue{impl: mh}
+}
+
+func (p PriorityQueue) Len() int { return p.impl.Len() }
+
+func (p *PriorityQueue) Push(el *PQItem) {
+ heap.Push(p.impl, el)
+}
+
+func (p *PriorityQueue) Pop() *PQItem {
+ el := heap.Pop(p.impl)
+ return el.(*PQItem)
+}
+
+func (p *PriorityQueue) Peek() *PQItem {
+ return (*p.impl)[0]
+}
+
+// Modifies the priority and value of an Item in the queue.
+func (p *PriorityQueue) Update(el *PQItem, priority int) {
+ heap.Remove(p.impl, el.index)
+ el.Priority = priority
+ heap.Push(p.impl, el)
+}
+
+func (p *PriorityQueue) Remove(el *PQItem) {
+ heap.Remove(p.impl, el.index)
+}
+
+// Actual Implementation using heap.Interface
+type pqImpl []*PQItem
+
+func (mh pqImpl) Len() int { return len(mh) }
+
+func (mh pqImpl) Less(i, j int) bool {
+ return mh[i].Priority < mh[j].Priority
+}
+
+func (mh pqImpl) Swap(i, j int) {
+ mh[i], mh[j] = mh[j], mh[i]
+ mh[i].index = i
+ mh[j].index = j
+}
+
+func (mh *pqImpl) Push(x interface{}) {
+ n := len(*mh)
+ item := x.(*PQItem)
+ item.index = n
+ *mh = append(*mh, item)
+}
+
+func (mh *pqImpl) Pop() interface{} {
+ old := *mh
+ n := len(old)
+ item := old[n-1]
+ item.index = -1 // for safety
+ *mh = old[0 : n-1]
+ return item
+}
diff --git a/internal/holsterv4/collections/priority_queue_test.go b/internal/holsterv4/collections/priority_queue_test.go
new file mode 100644
index 0000000..2a62606
--- /dev/null
+++ b/internal/holsterv4/collections/priority_queue_test.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2017 Mailgun Technologies Inc
+
+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 collections_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/vulcand/oxy/internal/holsterv4/collections"
+)
+
+func toPtr(i int) interface{} {
+ return &i
+}
+
+func toInt(i interface{}) int {
+ return *(i.(*int))
+}
+
+func TestPeek(t *testing.T) {
+ mh := collections.NewPriorityQueue()
+
+ el := &collections.PQItem{
+ Value: toPtr(1),
+ Priority: 5,
+ }
+
+ mh.Push(el)
+ assert.Equal(t, 1, toInt(mh.Peek().Value))
+ assert.Equal(t, 1, mh.Len())
+
+ el = &collections.PQItem{
+ Value: toPtr(2),
+ Priority: 1,
+ }
+ mh.Push(el)
+ assert.Equal(t, 2, mh.Len())
+ assert.Equal(t, 2, toInt(mh.Peek().Value))
+ assert.Equal(t, 2, toInt(mh.Peek().Value))
+ assert.Equal(t, 2, mh.Len())
+
+ el = mh.Pop()
+
+ assert.Equal(t, 2, toInt(el.Value))
+ assert.Equal(t, 1, mh.Len())
+ assert.Equal(t, 1, toInt(mh.Peek().Value))
+
+ mh.Pop()
+ assert.Equal(t, 0, mh.Len())
+}
+
+func TestUpdate(t *testing.T) {
+ mh := collections.NewPriorityQueue()
+ x := &collections.PQItem{
+ Value: toPtr(1),
+ Priority: 4,
+ }
+ y := &collections.PQItem{
+ Value: toPtr(2),
+ Priority: 3,
+ }
+ z := &collections.PQItem{
+ Value: toPtr(3),
+ Priority: 8,
+ }
+ mh.Push(x)
+ mh.Push(y)
+ mh.Push(z)
+ assert.Equal(t, 2, toInt(mh.Peek().Value))
+
+ mh.Update(z, 1)
+ assert.Equal(t, 3, toInt(mh.Peek().Value))
+
+ mh.Update(x, 0)
+ assert.Equal(t, 1, toInt(mh.Peek().Value))
+}
+
+func ExampleNewPriorityQueue() {
+ queue := collections.NewPriorityQueue()
+
+ queue.Push(&collections.PQItem{
+ Value: "thing3",
+ Priority: 3,
+ })
+
+ queue.Push(&collections.PQItem{
+ Value: "thing1",
+ Priority: 1,
+ })
+
+ queue.Push(&collections.PQItem{
+ Value: "thing2",
+ Priority: 2,
+ })
+
+ // Pops item off the queue according to the priority instead of the Push() order
+ item := queue.Pop()
+
+ fmt.Printf("Item: %s", item.Value.(string))
+
+ // Output: Item: thing1
+}
diff --git a/internal/holsterv4/collections/ttlmap.go b/internal/holsterv4/collections/ttlmap.go
new file mode 100644
index 0000000..44442d8
--- /dev/null
+++ b/internal/holsterv4/collections/ttlmap.go
@@ -0,0 +1,233 @@
+/*
+Copyright 2017 Mailgun Technologies Inc
+
+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 collections
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
+)
+
+type TTLMap struct {
+ // Optionally specifies a callback function to be
+ // executed when an entry has expired
+ OnExpire func(key string, i interface{})
+
+ capacity int
+ elements map[string]*mapElement
+ expiryTimes *PriorityQueue
+ mutex *sync.RWMutex
+}
+
+type mapElement struct {
+ key string
+ value interface{}
+ heapEl *PQItem
+}
+
+func NewTTLMap(capacity int) *TTLMap {
+ if capacity <= 0 {
+ capacity = 0
+ }
+
+ return &TTLMap{
+ capacity: capacity,
+ elements: make(map[string]*mapElement),
+ expiryTimes: NewPriorityQueue(),
+ mutex: &sync.RWMutex{},
+ }
+}
+
+func (m *TTLMap) Set(key string, value interface{}, ttlSeconds int) error {
+ expiryTime, err := m.toEpochSeconds(ttlSeconds)
+ if err != nil {
+ return err
+ }
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+ return m.set(key, value, expiryTime)
+}
+
+func (m *TTLMap) Len() int {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+ return len(m.elements)
+}
+
+func (m *TTLMap) Get(key string) (interface{}, bool) {
+ value, mapEl, expired := m.lockNGet(key)
+ if mapEl == nil {
+ return nil, false
+ }
+ if expired {
+ m.lockNDel(mapEl)
+ return nil, false
+ }
+ return value, true
+}
+
+func (m *TTLMap) Increment(key string, value int, ttlSeconds int) (int, error) {
+ expiryTime, err := m.toEpochSeconds(ttlSeconds)
+ if err != nil {
+ return 0, err
+ }
+
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ mapEl, expired := m.get(key)
+ if mapEl == nil || expired {
+ m.set(key, value, expiryTime)
+ return value, nil
+ }
+
+ currentValue, ok := mapEl.value.(int)
+ if !ok {
+ return 0, fmt.Errorf("Expected existing value to be integer, got %T", mapEl.value)
+ }
+
+ currentValue += value
+ m.set(key, currentValue, expiryTime)
+ return currentValue, nil
+}
+
+func (m *TTLMap) GetInt(key string) (int, bool, error) {
+ valueI, exists := m.Get(key)
+ if !exists {
+ return 0, false, nil
+ }
+ value, ok := valueI.(int)
+ if !ok {
+ return 0, false, fmt.Errorf("Expected existing value to be integer, got %T", valueI)
+ }
+ return value, true, nil
+}
+
+func (m *TTLMap) set(key string, value interface{}, expiryTime int) error {
+ if mapEl, ok := m.elements[key]; ok {
+ mapEl.value = value
+ m.expiryTimes.Update(mapEl.heapEl, expiryTime)
+ return nil
+ }
+
+ if len(m.elements) >= m.capacity {
+ m.freeSpace(1)
+ }
+ heapEl := &PQItem{
+ Priority: expiryTime,
+ }
+ mapEl := &mapElement{
+ key: key,
+ value: value,
+ heapEl: heapEl,
+ }
+ heapEl.Value = mapEl
+ m.elements[key] = mapEl
+ m.expiryTimes.Push(heapEl)
+ return nil
+}
+
+func (m *TTLMap) lockNGet(key string) (value interface{}, mapEl *mapElement, expired bool) {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ mapEl, expired = m.get(key)
+ value = nil
+ if mapEl != nil {
+ value = mapEl.value
+ }
+ return value, mapEl, expired
+}
+
+func (m *TTLMap) get(key string) (*mapElement, bool) {
+ mapEl, ok := m.elements[key]
+ if !ok {
+ return nil, false
+ }
+ now := int(clock.Now().Unix())
+ expired := mapEl.heapEl.Priority <= now
+ return mapEl, expired
+}
+
+func (m *TTLMap) lockNDel(mapEl *mapElement) {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ // Map element could have been updated. Now that we have a lock
+ // retrieve it again and check if it is still expired.
+ var ok bool
+ if mapEl, ok = m.elements[mapEl.key]; !ok {
+ return
+ }
+ now := int(clock.Now().Unix())
+ if mapEl.heapEl.Priority > now {
+ return
+ }
+
+ if m.OnExpire != nil {
+ m.OnExpire(mapEl.key, mapEl.value)
+ }
+
+ delete(m.elements, mapEl.key)
+ m.expiryTimes.Remove(mapEl.heapEl)
+}
+
+func (m *TTLMap) freeSpace(count int) {
+ removed := m.RemoveExpired(count)
+ if removed >= count {
+ return
+ }
+ m.RemoveLastUsed(count - removed)
+}
+
+func (m *TTLMap) RemoveExpired(iterations int) int {
+ removed := 0
+ now := int(clock.Now().Unix())
+ for i := 0; i < iterations; i += 1 {
+ if len(m.elements) == 0 {
+ break
+ }
+ heapEl := m.expiryTimes.Peek()
+ if heapEl.Priority > now {
+ break
+ }
+ m.expiryTimes.Pop()
+ mapEl := heapEl.Value.(*mapElement)
+ delete(m.elements, mapEl.key)
+ removed += 1
+ }
+ return removed
+}
+
+func (m *TTLMap) RemoveLastUsed(iterations int) {
+ for i := 0; i < iterations; i += 1 {
+ if len(m.elements) == 0 {
+ return
+ }
+ heapEl := m.expiryTimes.Pop()
+ mapEl := heapEl.Value.(*mapElement)
+ delete(m.elements, mapEl.key)
+ }
+}
+
+func (m *TTLMap) toEpochSeconds(ttlSeconds int) (int, error) {
+ if ttlSeconds <= 0 {
+ return 0, fmt.Errorf("ttlSeconds should be >= 0, got %d", ttlSeconds)
+ }
+ return int(clock.Now().Add(time.Second * time.Duration(ttlSeconds)).Unix()), nil
+}
diff --git a/internal/holsterv4/collections/ttlmap_test.go b/internal/holsterv4/collections/ttlmap_test.go
new file mode 100644
index 0000000..5c1bac1
--- /dev/null
+++ b/internal/holsterv4/collections/ttlmap_test.go
@@ -0,0 +1,337 @@
+/*
+Copyright 2017 Mailgun Technologies Inc
+
+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 collections
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
+)
+
+type TTLMapSuite struct {
+ suite.Suite
+}
+
+func TestTTLMapSuite(t *testing.T) {
+ suite.Run(t, new(TTLMapSuite))
+}
+
+func (s *TTLMapSuite) SetupTest() {
+ clock.Freeze(clock.Date(2012, 3, 4, 5, 6, 7, 0, clock.UTC))
+}
+
+func (s *TTLMapSuite) TearDownSuite() {
+ clock.Unfreeze()
+}
+
+func (s *TTLMapSuite) TestSetWrong() {
+ m := NewTTLMap(1)
+
+ err := m.Set("a", 1, -1)
+ s.Require().EqualError(err, "ttlSeconds should be >= 0, got -1")
+
+ err = m.Set("a", 1, 0)
+ s.Require().EqualError(err, "ttlSeconds should be >= 0, got 0")
+
+ _, err = m.Increment("a", 1, 0)
+ s.Require().EqualError(err, "ttlSeconds should be >= 0, got 0")
+
+ _, err = m.Increment("a", 1, -1)
+ s.Require().EqualError(err, "ttlSeconds should be >= 0, got -1")
+}
+
+func (s *TTLMapSuite) TestRemoveExpiredEmpty() {
+ m := NewTTLMap(1)
+ m.RemoveExpired(100)
+}
+
+func (s *TTLMapSuite) TestRemoveLastUsedEmpty() {
+ m := NewTTLMap(1)
+ m.RemoveLastUsed(100)
+}
+
+func (s *TTLMapSuite) TestGetSetExpire() {
+ m := NewTTLMap(1)
+
+ err := m.Set("a", 1, 1)
+ s.Require().Equal(nil, err)
+
+ valI, exists := m.Get("a")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(1, valI)
+
+ clock.Advance(1 * clock.Second)
+
+ _, exists = m.Get("a")
+ s.Require().Equal(false, exists)
+}
+
+func (s *TTLMapSuite) TestSetOverwrite() {
+ m := NewTTLMap(1)
+
+ err := m.Set("o", 1, 1)
+ s.Require().Equal(nil, err)
+
+ valI, exists := m.Get("o")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(1, valI)
+
+ err = m.Set("o", 2, 1)
+ s.Require().Equal(nil, err)
+
+ valI, exists = m.Get("o")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(2, valI)
+}
+
+func (s *TTLMapSuite) TestRemoveExpiredEdgeCase() {
+ m := NewTTLMap(1)
+
+ err := m.Set("a", 1, 1)
+ s.Require().Equal(nil, err)
+
+ clock.Advance(1 * clock.Second)
+
+ err = m.Set("b", 2, 1)
+ s.Require().Equal(nil, err)
+
+ valI, exists := m.Get("a")
+ s.Require().Equal(false, exists)
+
+ valI, exists = m.Get("b")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(2, valI)
+
+ s.Require().Equal(1, m.Len())
+}
+
+func (s *TTLMapSuite) TestRemoveOutOfCapacity() {
+ m := NewTTLMap(2)
+
+ err := m.Set("a", 1, 5)
+ s.Require().Equal(nil, err)
+
+ clock.Advance(1 * clock.Second)
+
+ err = m.Set("b", 2, 6)
+ s.Require().Equal(nil, err)
+
+ err = m.Set("c", 3, 10)
+ s.Require().Equal(nil, err)
+
+ valI, exists := m.Get("a")
+ s.Require().Equal(false, exists)
+
+ valI, exists = m.Get("b")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(2, valI)
+
+ valI, exists = m.Get("c")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(3, valI)
+
+ s.Require().Equal(2, m.Len())
+}
+
+func (s *TTLMapSuite) TestGetNotExists() {
+ m := NewTTLMap(1)
+ _, exists := m.Get("a")
+ s.Require().Equal(false, exists)
+}
+
+func (s *TTLMapSuite) TestGetIntNotExists() {
+ m := NewTTLMap(1)
+ _, exists, err := m.GetInt("a")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(false, exists)
+}
+
+func (s *TTLMapSuite) TestGetInvalidType() {
+ m := NewTTLMap(1)
+ m.Set("a", "banana", 5)
+
+ _, _, err := m.GetInt("a")
+ s.Require().EqualError(err, "Expected existing value to be integer, got string")
+
+ _, err = m.Increment("a", 4, 1)
+ s.Require().EqualError(err, "Expected existing value to be integer, got string")
+}
+
+func (s *TTLMapSuite) TestIncrementGetExpire() {
+ m := NewTTLMap(1)
+
+ m.Increment("a", 5, 1)
+ val, exists, err := m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(5, val)
+
+ clock.Advance(1 * clock.Second)
+
+ m.Increment("a", 4, 1)
+ val, exists, err = m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(4, val)
+}
+
+func (s *TTLMapSuite) TestIncrementOverwrite() {
+ m := NewTTLMap(1)
+
+ m.Increment("a", 5, 1)
+ val, exists, err := m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(5, val)
+
+ m.Increment("a", 4, 1)
+ val, exists, err = m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(9, val)
+}
+
+func (s *TTLMapSuite) TestIncrementOutOfCapacity() {
+ m := NewTTLMap(1)
+
+ m.Increment("a", 5, 1)
+ val, exists, err := m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(5, val)
+
+ m.Increment("b", 4, 1)
+ val, exists, err = m.GetInt("b")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(4, val)
+
+ val, exists, err = m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(false, exists)
+}
+
+func (s *TTLMapSuite) TestIncrementRemovesExpired() {
+ m := NewTTLMap(2)
+
+ m.Increment("a", 1, 1)
+ m.Increment("b", 2, 2)
+
+ clock.Advance(1 * clock.Second)
+ m.Increment("c", 3, 3)
+
+ val, exists, err := m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(false, exists)
+
+ val, exists, err = m.GetInt("b")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(2, val)
+
+ val, exists, err = m.GetInt("c")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(3, val)
+}
+
+func (s *TTLMapSuite) TestIncrementRemovesLastUsed() {
+ m := NewTTLMap(2)
+
+ m.Increment("a", 1, 10)
+ m.Increment("b", 2, 11)
+ m.Increment("c", 3, 12)
+
+ val, exists, err := m.GetInt("a")
+
+ s.Require().Equal(nil, err)
+ s.Require().Equal(false, exists)
+
+ val, exists, err = m.GetInt("b")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+
+ s.Require().Equal(2, val)
+
+ val, exists, err = m.GetInt("c")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(3, val)
+}
+
+func (s *TTLMapSuite) TestIncrementUpdatesTtl() {
+ m := NewTTLMap(1)
+
+ m.Increment("a", 1, 1)
+ m.Increment("a", 1, 10)
+
+ clock.Advance(1 * clock.Second)
+
+ val, exists, err := m.GetInt("a")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(2, val)
+}
+
+func (s *TTLMapSuite) TestUpdate() {
+ m := NewTTLMap(1)
+
+ m.Increment("a", 1, 1)
+ m.Increment("a", 1, 10)
+
+ clock.Advance(1 * clock.Second)
+
+ val, exists, err := m.GetInt("a")
+ s.Require().Equal(nil, err)
+ s.Require().Equal(true, exists)
+ s.Require().Equal(2, val)
+}
+
+func (s *TTLMapSuite) TestCallOnExpire() {
+ var called bool
+ var key string
+ var val interface{}
+ m := NewTTLMap(1)
+ m.OnExpire = func(k string, el interface{}) {
+ called = true
+ key = k
+ val = el
+ }
+
+ err := m.Set("a", 1, 1)
+ s.Require().Equal(nil, err)
+
+ valI, exists := m.Get("a")
+ s.Require().Equal(true, exists)
+ s.Require().Equal(1, valI)
+
+ clock.Advance(1 * clock.Second)
+
+ _, exists = m.Get("a")
+ s.Require().Equal(false, exists)
+ s.Require().Equal(true, called)
+ s.Require().Equal("a", key)
+ s.Require().Equal(1, val)
+}
diff --git a/memmetrics/anomaly_test.go b/memmetrics/anomaly_test.go
index 87b5d7c..6f89f4d 100644
--- a/memmetrics/anomaly_test.go
+++ b/memmetrics/anomaly_test.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
func TestMedian(t *testing.T) {
@@ -175,20 +176,20 @@ func TestSplitLatencies(t *testing.T) {
values := make([]time.Duration, len(test.values))
for i, d := range test.values {
- values[i] = time.Millisecond * time.Duration(d)
+ values[i] = clock.Millisecond * time.Duration(d)
}
- good, bad := SplitLatencies(values, time.Millisecond)
+ good, bad := SplitLatencies(values, clock.Millisecond)
vgood := make(map[time.Duration]bool, len(test.good))
for _, v := range test.good {
- vgood[time.Duration(v)*time.Millisecond] = true
+ vgood[time.Duration(v)*clock.Millisecond] = true
}
assert.Equal(t, vgood, good)
vbad := make(map[time.Duration]bool, len(test.bad))
for _, v := range test.bad {
- vbad[time.Duration(v)*time.Millisecond] = true
+ vbad[time.Duration(v)*clock.Millisecond] = true
}
assert.Equal(t, vbad, bad)
})
diff --git a/memmetrics/counter.go b/memmetrics/counter.go
index 4faf905..853acb7 100644
--- a/memmetrics/counter.go
+++ b/memmetrics/counter.go
@@ -4,37 +4,28 @@ import (
"fmt"
"time"
- "github.com/mailgun/timetools"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
type rcOptSetter func(*RollingCounter) error
-// CounterClock defines a counter clock
-func CounterClock(c timetools.TimeProvider) rcOptSetter {
- return func(r *RollingCounter) error {
- r.clock = c
- return nil
- }
-}
-
-// RollingCounter Calculates in memory failure rate of an endpoint using rolling window of a predefined size
+// RollingCounter Calculates in memory failure rate of an endpoint using rolling window of a predefined size.
type RollingCounter struct {
- clock timetools.TimeProvider
resolution time.Duration
values []int
countedBuckets int // how many samples in different buckets have we collected so far
lastBucket int // last recorded bucket
- lastUpdated time.Time
+ lastUpdated clock.Time
}
// NewCounter creates a counter with fixed amount of buckets that are rotated every resolution period.
// E.g. 10 buckets with 1 second means that every new second the bucket is refreshed, so it maintains 10 second rolling window.
-// By default creates a bucket with 10 buckets and 1 second resolution
+// By default creates a bucket with 10 buckets and 1 second resolution.
func NewCounter(buckets int, resolution time.Duration, options ...rcOptSetter) (*RollingCounter, error) {
if buckets <= 0 {
return nil, fmt.Errorf("Buckets should be >= 0")
}
- if resolution < time.Second {
+ if resolution < clock.Second {
return nil, fmt.Errorf("Resolution should be larger than a second")
}
@@ -51,26 +42,21 @@ func NewCounter(buckets int, resolution time.Duration, options ...rcOptSetter) (
}
}
- if rc.clock == nil {
- rc.clock = &timetools.RealTime{}
- }
-
return rc, nil
}
-// Append append a counter
+// Append append a counter.
func (c *RollingCounter) Append(o *RollingCounter) error {
c.Inc(int(o.Count()))
return nil
}
-// Clone clone a counter
+// Clone clone a counter.
func (c *RollingCounter) Clone() *RollingCounter {
c.cleanup()
other := &RollingCounter{
resolution: c.resolution,
values: make([]int, len(c.values)),
- clock: c.clock,
lastBucket: c.lastBucket,
lastUpdated: c.lastUpdated,
}
@@ -78,50 +64,50 @@ func (c *RollingCounter) Clone() *RollingCounter {
return other
}
-// Reset reset a counter
+// Reset reset a counter.
func (c *RollingCounter) Reset() {
c.lastBucket = -1
c.countedBuckets = 0
- c.lastUpdated = time.Time{}
+ c.lastUpdated = clock.Time{}
for i := range c.values {
c.values[i] = 0
}
}
-// CountedBuckets gets counted buckets
+// CountedBuckets gets counted buckets.
func (c *RollingCounter) CountedBuckets() int {
return c.countedBuckets
}
-// Count counts
+// Count counts.
func (c *RollingCounter) Count() int64 {
c.cleanup()
return c.sum()
}
-// Resolution gets resolution
+// Resolution gets resolution.
func (c *RollingCounter) Resolution() time.Duration {
return c.resolution
}
-// Buckets gets buckets
+// Buckets gets buckets.
func (c *RollingCounter) Buckets() int {
return len(c.values)
}
-// WindowSize gets windows size
+// WindowSize gets windows size.
func (c *RollingCounter) WindowSize() time.Duration {
return time.Duration(len(c.values)) * c.resolution
}
-// Inc increment counter
+// Inc increment counter.
func (c *RollingCounter) Inc(v int) {
c.cleanup()
c.incBucketValue(v)
}
func (c *RollingCounter) incBucketValue(v int) {
- now := c.clock.UtcNow()
+ now := clock.Now().UTC()
bucket := c.getBucket(now)
c.values[bucket] += v
c.lastUpdated = now
@@ -136,14 +122,14 @@ func (c *RollingCounter) incBucketValue(v int) {
}
}
-// Returns the number in the moving window bucket that this slot occupies
+// Returns the number in the moving window bucket that this slot occupies.
func (c *RollingCounter) getBucket(t time.Time) int {
return int(t.Truncate(c.resolution).Unix() % int64(len(c.values)))
}
-// Reset buckets that were not updated
+// Reset buckets that were not updated.
func (c *RollingCounter) cleanup() {
- now := c.clock.UtcNow()
+ now := clock.Now().UTC()
for i := 0; i < len(c.values); i++ {
now = now.Add(time.Duration(-1*i) * c.resolution)
if now.Truncate(c.resolution).After(c.lastUpdated.Truncate(c.resolution)) {
diff --git a/memmetrics/counter_test.go b/memmetrics/counter_test.go
index eb07a5c..099b236 100644
--- a/memmetrics/counter_test.go
+++ b/memmetrics/counter_test.go
@@ -2,30 +2,27 @@ package memmetrics
import (
"testing"
- "time"
- "github.com/mailgun/timetools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
func TestCloneExpired(t *testing.T) {
- clockTest := &timetools.FreezedTime{
- CurrentTime: time.Date(2012, 3, 4, 5, 6, 7, 0, time.UTC),
- }
+ clock.Freeze(clock.Date(2012, 3, 4, 5, 6, 7, 0, clock.UTC))
- cnt, err := NewCounter(3, time.Second, CounterClock(clockTest))
+ cnt, err := NewCounter(3, clock.Second)
require.NoError(t, err)
cnt.Inc(1)
- clockTest.Sleep(time.Second)
+ clock.Advance(clock.Second)
cnt.Inc(1)
- clockTest.Sleep(time.Second)
+ clock.Advance(clock.Second)
cnt.Inc(1)
- clockTest.Sleep(time.Second)
+ clock.Advance(clock.Second)
out := cnt.Clone()
assert.EqualValues(t, 2, out.Count())
diff --git a/memmetrics/histogram.go b/memmetrics/histogram.go
index 2c3aa76..9cfde95 100644
--- a/memmetrics/histogram.go
+++ b/memmetrics/histogram.go
@@ -4,11 +4,11 @@ import (
"fmt"
"time"
- "github.com/codahale/hdrhistogram"
- "github.com/mailgun/timetools"
+ "github.com/HdrHistogram/hdrhistogram-go"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
-// HDRHistogram is a tiny wrapper around github.com/codahale/hdrhistogram that provides convenience functions for measuring http latencies
+// HDRHistogram is a tiny wrapper around github.com/HdrHistogram/hdrhistogram-go that provides convenience functions for measuring http latencies.
type HDRHistogram struct {
// lowest trackable value
low int64
@@ -20,7 +20,7 @@ type HDRHistogram struct {
h *hdrhistogram.Histogram
}
-// NewHDRHistogram creates a new HDRHistogram
+// NewHDRHistogram creates a new HDRHistogram.
func NewHDRHistogram(low, high int64, sigfigs int) (h *HDRHistogram, err error) {
defer func() {
if msg := recover(); msg != nil {
@@ -35,7 +35,7 @@ func NewHDRHistogram(low, high int64, sigfigs int) (h *HDRHistogram, err error)
}, nil
}
-// Export export a HDRHistogram
+// Export export a HDRHistogram.
func (h *HDRHistogram) Export() *HDRHistogram {
var hist *hdrhistogram.Histogram
if h.h != nil {
@@ -45,32 +45,32 @@ func (h *HDRHistogram) Export() *HDRHistogram {
return &HDRHistogram{low: h.low, high: h.high, sigfigs: h.sigfigs, h: hist}
}
-// LatencyAtQuantile sets latency at quantile with microsecond precision
+// LatencyAtQuantile sets latency at quantile with microsecond precision.
func (h *HDRHistogram) LatencyAtQuantile(q float64) time.Duration {
- return time.Duration(h.ValueAtQuantile(q)) * time.Microsecond
+ return time.Duration(h.ValueAtQuantile(q)) * clock.Microsecond
}
-// RecordLatencies Records latencies with microsecond precision
+// RecordLatencies Records latencies with microsecond precision.
func (h *HDRHistogram) RecordLatencies(d time.Duration, n int64) error {
- return h.RecordValues(int64(d/time.Microsecond), n)
+ return h.RecordValues(int64(d/clock.Microsecond), n)
}
-// Reset reset a HDRHistogram
+// Reset reset a HDRHistogram.
func (h *HDRHistogram) Reset() {
h.h.Reset()
}
-// ValueAtQuantile sets value at quantile
+// ValueAtQuantile sets value at quantile.
func (h *HDRHistogram) ValueAtQuantile(q float64) int64 {
return h.h.ValueAtQuantile(q)
}
-// RecordValues sets record values
+// RecordValues sets record values.
func (h *HDRHistogram) RecordValues(v, n int64) error {
return h.h.RecordValues(v, n)
}
-// Merge merge a HDRHistogram
+// Merge merge a HDRHistogram.
func (h *HDRHistogram) Merge(other *HDRHistogram) error {
if other == nil {
return fmt.Errorf("other is nil")
@@ -81,29 +81,20 @@ func (h *HDRHistogram) Merge(other *HDRHistogram) error {
type rhOptSetter func(r *RollingHDRHistogram) error
-// RollingClock sets a clock
-func RollingClock(clock timetools.TimeProvider) rhOptSetter {
- return func(r *RollingHDRHistogram) error {
- r.clock = clock
- return nil
- }
-}
-
// RollingHDRHistogram holds multiple histograms and rotates every period.
// It provides resulting histogram as a result of a call of 'Merged' function.
type RollingHDRHistogram struct {
idx int
- lastRoll time.Time
+ lastRoll clock.Time
period time.Duration
bucketCount int
low int64
high int64
sigfigs int
buckets []*HDRHistogram
- clock timetools.TimeProvider
}
-// NewRollingHDRHistogram created a new RollingHDRHistogram
+// NewRollingHDRHistogram created a new RollingHDRHistogram.
func NewRollingHDRHistogram(low, high int64, sigfigs int, period time.Duration, bucketCount int, options ...rhOptSetter) (*RollingHDRHistogram, error) {
rh := &RollingHDRHistogram{
bucketCount: bucketCount,
@@ -119,10 +110,6 @@ func NewRollingHDRHistogram(low, high int64, sigfigs int, period time.Duration,
}
}
- if rh.clock == nil {
- rh.clock = &timetools.RealTime{}
- }
-
buckets := make([]*HDRHistogram, rh.bucketCount)
for i := range buckets {
h, err := NewHDRHistogram(low, high, sigfigs)
@@ -135,7 +122,7 @@ func NewRollingHDRHistogram(low, high int64, sigfigs int, period time.Duration,
return rh, nil
}
-// Export export a RollingHDRHistogram
+// Export export a RollingHDRHistogram.
func (r *RollingHDRHistogram) Export() *RollingHDRHistogram {
export := &RollingHDRHistogram{}
export.idx = r.idx
@@ -145,7 +132,6 @@ func (r *RollingHDRHistogram) Export() *RollingHDRHistogram {
export.low = r.low
export.high = r.high
export.sigfigs = r.sigfigs
- export.clock = r.clock
exportBuckets := make([]*HDRHistogram, len(r.buckets))
for i, hist := range r.buckets {
@@ -156,7 +142,7 @@ func (r *RollingHDRHistogram) Export() *RollingHDRHistogram {
return export
}
-// Append append a RollingHDRHistogram
+// Append append a RollingHDRHistogram.
func (r *RollingHDRHistogram) Append(o *RollingHDRHistogram) error {
if r.bucketCount != o.bucketCount || r.period != o.period || r.low != o.low || r.high != o.high || r.sigfigs != o.sigfigs {
return fmt.Errorf("can't merge")
@@ -170,10 +156,10 @@ func (r *RollingHDRHistogram) Append(o *RollingHDRHistogram) error {
return nil
}
-// Reset reset a RollingHDRHistogram
+// Reset reset a RollingHDRHistogram.
func (r *RollingHDRHistogram) Reset() {
r.idx = 0
- r.lastRoll = r.clock.UtcNow()
+ r.lastRoll = clock.Now().UTC()
for _, b := range r.buckets {
b.Reset()
}
@@ -184,7 +170,7 @@ func (r *RollingHDRHistogram) rotate() {
r.buckets[r.idx].Reset()
}
-// Merged gets merged histogram
+// Merged gets merged histogram.
func (r *RollingHDRHistogram) Merged() (*HDRHistogram, error) {
m, err := NewHDRHistogram(r.low, r.high, r.sigfigs)
if err != nil {
@@ -199,19 +185,19 @@ func (r *RollingHDRHistogram) Merged() (*HDRHistogram, error) {
}
func (r *RollingHDRHistogram) getHist() *HDRHistogram {
- if r.clock.UtcNow().Sub(r.lastRoll) >= r.period {
+ if clock.Now().UTC().Sub(r.lastRoll) >= r.period {
r.rotate()
- r.lastRoll = r.clock.UtcNow()
+ r.lastRoll = clock.Now().UTC()
}
return r.buckets[r.idx]
}
-// RecordLatencies sets records latencies
+// RecordLatencies sets records latencies.
func (r *RollingHDRHistogram) RecordLatencies(v time.Duration, n int64) error {
return r.getHist().RecordLatencies(v, n)
}
-// RecordValues set record values
+// RecordValues set record values.
func (r *RollingHDRHistogram) RecordValues(v, n int64) error {
return r.getHist().RecordValues(v, n)
}
diff --git a/memmetrics/histogram_test.go b/memmetrics/histogram_test.go
index ced9e7f..3c27698 100644
--- a/memmetrics/histogram_test.go
+++ b/memmetrics/histogram_test.go
@@ -2,11 +2,11 @@ package memmetrics
import (
"testing"
- "time"
- "github.com/codahale/hdrhistogram"
+ "github.com/HdrHistogram/hdrhistogram-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
@@ -27,11 +27,6 @@ func TestMerge(t *testing.T) {
assert.EqualValues(t, 2, a.ValueAtQuantile(100))
}
-func TestInvalidParams(t *testing.T) {
- _, err := NewHDRHistogram(1, 3600000, 0)
- require.Error(t, err)
-}
-
func TestMergeNil(t *testing.T) {
a, err := NewHDRHistogram(1, 3600000, 1)
require.NoError(t, err)
@@ -40,15 +35,16 @@ func TestMergeNil(t *testing.T) {
}
func TestRotation(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
h, err := NewRollingHDRHistogram(
- 1, // min value
- 3600000, // max value
- 3, // significant figures
- time.Second, // 1 second is a rolling period
- 2, // 2 histograms in a window
- RollingClock(clock))
+ 1, // min value
+ 3600000, // max value
+ 3, // significant figures
+ clock.Second,
+ 2, // 2 histograms in a window
+ )
require.NoError(t, err)
require.NotNil(t, h)
@@ -60,7 +56,7 @@ func TestRotation(t *testing.T) {
require.NoError(t, err)
assert.EqualValues(t, 5, m.ValueAtQuantile(100))
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
require.NoError(t, h.RecordValues(2, 1))
require.NoError(t, h.RecordValues(1, 1))
@@ -69,7 +65,7 @@ func TestRotation(t *testing.T) {
assert.EqualValues(t, 5, m.ValueAtQuantile(100))
// rotate, this means that the old value would evaporate
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
require.NoError(t, h.RecordValues(1, 1))
@@ -79,15 +75,16 @@ func TestRotation(t *testing.T) {
}
func TestReset(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
h, err := NewRollingHDRHistogram(
- 1, // min value
- 3600000, // max value
- 3, // significant figures
- time.Second, // 1 second is a rolling period
- 2, // 2 histograms in a window
- RollingClock(clock))
+ 1, // min value
+ 3600000, // max value
+ 3, // significant figures
+ clock.Second,
+ 2, // 2 histograms in a window
+ )
require.NoError(t, err)
require.NotNil(t, h)
@@ -98,7 +95,7 @@ func TestReset(t *testing.T) {
require.NoError(t, err)
assert.EqualValues(t, 5, m.ValueAtQuantile(100))
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
require.NoError(t, h.RecordValues(2, 1))
require.NoError(t, h.RecordValues(1, 1))
@@ -114,14 +111,13 @@ func TestReset(t *testing.T) {
require.NoError(t, err)
assert.EqualValues(t, 5, m.ValueAtQuantile(100))
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
require.NoError(t, h.RecordValues(2, 1))
require.NoError(t, h.RecordValues(1, 1))
m, err = h.Merged()
require.NoError(t, err)
assert.EqualValues(t, 5, m.ValueAtQuantile(100))
-
}
func TestHDRHistogramExportReturnsNewCopy(t *testing.T) {
@@ -148,37 +144,37 @@ func TestHDRHistogramExportReturnsNewCopy(t *testing.T) {
}
func TestRollingHDRHistogramExportReturnsNewCopy(t *testing.T) {
- origTime := time.Now()
+ origTime := clock.Now()
+
+ done := testutils.FreezeTime()
+ defer done()
a := RollingHDRHistogram{
idx: 1,
lastRoll: origTime,
- period: 2 * time.Second,
+ period: 2 * clock.Second,
bucketCount: 3,
low: 4,
high: 5,
sigfigs: 1,
buckets: []*HDRHistogram{},
- clock: testutils.GetClock(),
}
b := a.Export()
a.idx = 11
- a.lastRoll = time.Now().Add(1 * time.Minute)
- a.period = 12 * time.Second
+ a.lastRoll = clock.Now().Add(1 * clock.Minute)
+ a.period = 12 * clock.Second
a.bucketCount = 13
a.low = 14
a.high = 15
a.sigfigs = 1
a.buckets = nil
- a.clock = nil
assert.Equal(t, 1, b.idx)
assert.Equal(t, origTime, b.lastRoll)
- assert.Equal(t, 2*time.Second, b.period)
+ assert.Equal(t, 2*clock.Second, b.period)
assert.Equal(t, 3, b.bucketCount)
assert.Equal(t, int64(4), b.low)
assert.EqualValues(t, 5, b.high)
assert.NotNil(t, b.buckets)
- assert.NotNil(t, b.clock)
}
diff --git a/memmetrics/ratio.go b/memmetrics/ratio.go
index ecfd503..4c22021 100644
--- a/memmetrics/ratio.go
+++ b/memmetrics/ratio.go
@@ -1,29 +1,16 @@
package memmetrics
-import (
- "time"
-
- "github.com/mailgun/timetools"
-)
+import "time"
type ratioOptSetter func(r *RatioCounter) error
-// RatioClock sets a clock
-func RatioClock(clock timetools.TimeProvider) ratioOptSetter {
- return func(r *RatioCounter) error {
- r.clock = clock
- return nil
- }
-}
-
-// RatioCounter calculates a ratio of a/a+b over a rolling window of predefined buckets
+// RatioCounter calculates a ratio of a/a+b over a rolling window of predefined buckets.
type RatioCounter struct {
- clock timetools.TimeProvider
- a *RollingCounter
- b *RollingCounter
+ a *RollingCounter
+ b *RollingCounter
}
-// NewRatioCounter creates a new RatioCounter
+// NewRatioCounter creates a new RatioCounter.
func NewRatioCounter(buckets int, resolution time.Duration, options ...ratioOptSetter) (*RatioCounter, error) {
rc := &RatioCounter{}
@@ -33,16 +20,12 @@ func NewRatioCounter(buckets int, resolution time.Duration, options ...ratioOptS
}
}
- if rc.clock == nil {
- rc.clock = &timetools.RealTime{}
- }
-
- a, err := NewCounter(buckets, resolution, CounterClock(rc.clock))
+ a, err := NewCounter(buckets, resolution)
if err != nil {
return nil, err
}
- b, err := NewCounter(buckets, resolution, CounterClock(rc.clock))
+ b, err := NewCounter(buckets, resolution)
if err != nil {
return nil, err
}
@@ -52,48 +35,48 @@ func NewRatioCounter(buckets int, resolution time.Duration, options ...ratioOptS
return rc, nil
}
-// Reset reset the counter
+// Reset reset the counter.
func (r *RatioCounter) Reset() {
r.a.Reset()
r.b.Reset()
}
-// IsReady returns true if the counter is ready
+// IsReady returns true if the counter is ready.
func (r *RatioCounter) IsReady() bool {
return r.a.countedBuckets+r.b.countedBuckets >= len(r.a.values)
}
-// CountA gets count A
+// CountA gets count A.
func (r *RatioCounter) CountA() int64 {
return r.a.Count()
}
-// CountB gets count B
+// CountB gets count B.
func (r *RatioCounter) CountB() int64 {
return r.b.Count()
}
-// Resolution gets resolution
+// Resolution gets resolution.
func (r *RatioCounter) Resolution() time.Duration {
return r.a.Resolution()
}
-// Buckets gets buckets
+// Buckets gets buckets.
func (r *RatioCounter) Buckets() int {
return r.a.Buckets()
}
-// WindowSize gets windows size
+// WindowSize gets windows size.
func (r *RatioCounter) WindowSize() time.Duration {
return r.a.WindowSize()
}
-// ProcessedCount gets processed count
+// ProcessedCount gets processed count.
func (r *RatioCounter) ProcessedCount() int64 {
return r.CountA() + r.CountB()
}
-// Ratio gets ratio
+// Ratio gets ratio.
func (r *RatioCounter) Ratio() float64 {
a := r.a.Count()
b := r.b.Count()
@@ -104,34 +87,34 @@ func (r *RatioCounter) Ratio() float64 {
return float64(a) / float64(a+b)
}
-// IncA increment counter A
+// IncA increment counter A.
func (r *RatioCounter) IncA(v int) {
r.a.Inc(v)
}
-// IncB increment counter B
+// IncB increment counter B.
func (r *RatioCounter) IncB(v int) {
r.b.Inc(v)
}
-// TestMeter a test meter
+// TestMeter a test meter.
type TestMeter struct {
Rate float64
NotReady bool
WindowSize time.Duration
}
-// GetWindowSize gets windows size
+// GetWindowSize gets windows size.
func (tm *TestMeter) GetWindowSize() time.Duration {
return tm.WindowSize
}
-// IsReady returns true if the meter is ready
+// IsReady returns true if the meter is ready.
func (tm *TestMeter) IsReady() bool {
return !tm.NotReady
}
-// GetRate gets rate
+// GetRate gets rate.
func (tm *TestMeter) GetRate() float64 {
return tm.Rate
}
diff --git a/memmetrics/ratio_test.go b/memmetrics/ratio_test.go
index 12a6d4d..9c664a6 100644
--- a/memmetrics/ratio_test.go
+++ b/memmetrics/ratio_test.go
@@ -2,43 +2,47 @@ package memmetrics
import (
"testing"
- "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
func TestNewRatioCounterInvalidParams(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Bad buckets count
- _, err := NewRatioCounter(0, time.Second, RatioClock(clock))
+ _, err := NewRatioCounter(0, clock.Second)
require.Error(t, err)
// Too precise resolution
- _, err = NewRatioCounter(10, time.Millisecond, RatioClock(clock))
+ _, err = NewRatioCounter(10, clock.Millisecond)
require.Error(t, err)
}
func TestNotReady(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// No data
- fr, err := NewRatioCounter(10, time.Second, RatioClock(clock))
+ fr, err := NewRatioCounter(10, clock.Second)
require.NoError(t, err)
assert.Equal(t, false, fr.IsReady())
assert.Equal(t, 0.0, fr.Ratio())
// Not enough data
- fr, err = NewRatioCounter(10, time.Second, RatioClock(clock))
+ fr, err = NewRatioCounter(10, clock.Second)
require.NoError(t, err)
fr.CountA()
assert.Equal(t, false, fr.IsReady())
}
func TestNoB(t *testing.T) {
- fr, err := NewRatioCounter(1, time.Second, RatioClock(testutils.GetClock()))
+ done := testutils.FreezeTime()
+ defer done()
+ fr, err := NewRatioCounter(1, clock.Second)
require.NoError(t, err)
fr.IncA(1)
assert.Equal(t, true, fr.IsReady())
@@ -46,25 +50,29 @@ func TestNoB(t *testing.T) {
}
func TestNoA(t *testing.T) {
- fr, err := NewRatioCounter(1, time.Second, RatioClock(testutils.GetClock()))
+ done := testutils.FreezeTime()
+ defer done()
+
+ fr, err := NewRatioCounter(1, clock.Second)
require.NoError(t, err)
fr.IncB(1)
assert.Equal(t, true, fr.IsReady())
assert.Equal(t, 0.0, fr.Ratio())
}
-// Make sure that data is properly calculated over several buckets
+// Make sure that data is properly calculated over several buckets.
func TestMultipleBuckets(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- fr, err := NewRatioCounter(3, time.Second, RatioClock(clock))
+ fr, err := NewRatioCounter(3, clock.Second)
require.NoError(t, err)
fr.IncB(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
assert.Equal(t, true, fr.IsReady())
@@ -72,23 +80,24 @@ func TestMultipleBuckets(t *testing.T) {
}
// Make sure that data is properly calculated over several buckets
-// When we overwrite old data when the window is rolling
+// When we overwrite old data when the window is rolling.
func TestOverwriteBuckets(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- fr, err := NewRatioCounter(3, time.Second, RatioClock(clock))
+ fr, err := NewRatioCounter(3, clock.Second)
require.NoError(t, err)
fr.IncB(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
// This time we should overwrite the old data points
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
fr.IncB(2)
@@ -97,28 +106,29 @@ func TestOverwriteBuckets(t *testing.T) {
}
// Make sure we cleanup the data after periods of inactivity
-// So it does not mess up the stats
+// So it does not mess up the stats.
func TestInactiveBuckets(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- fr, err := NewRatioCounter(3, time.Second, RatioClock(clock))
+ fr, err := NewRatioCounter(3, clock.Second)
require.NoError(t, err)
fr.IncB(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
// This time we should overwrite the old data points with new data
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
fr.IncB(2)
// Jump to the last bucket and change the data
- clock.CurrentTime = clock.CurrentTime.Add(time.Second * 2)
+ clock.Advance(clock.Second * 2)
fr.IncB(1)
assert.Equal(t, true, fr.IsReady())
@@ -126,27 +136,31 @@ func TestInactiveBuckets(t *testing.T) {
}
func TestLongPeriodsOfInactivity(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- fr, err := NewRatioCounter(2, time.Second, RatioClock(clock))
+ fr, err := NewRatioCounter(2, clock.Second)
require.NoError(t, err)
fr.IncB(1)
- clock.CurrentTime = clock.CurrentTime.Add(time.Second)
+ clock.Advance(clock.Second)
fr.IncA(1)
assert.Equal(t, true, fr.IsReady())
assert.Equal(t, 0.5, fr.Ratio())
// This time we should overwrite all data points
- clock.CurrentTime = clock.CurrentTime.Add(100 * time.Second)
+ clock.Advance(100 * clock.Second)
fr.IncA(1)
assert.Equal(t, 1.0, fr.Ratio())
}
func TestNewRatioCounterReset(t *testing.T) {
- fr, err := NewRatioCounter(1, time.Second, RatioClock(testutils.GetClock()))
+ done := testutils.FreezeTime()
+ defer done()
+
+ fr, err := NewRatioCounter(1, clock.Second)
require.NoError(t, err)
fr.IncB(1)
diff --git a/memmetrics/roundtrip.go b/memmetrics/roundtrip.go
index 34b3969..b241634 100644
--- a/memmetrics/roundtrip.go
+++ b/memmetrics/roundtrip.go
@@ -6,7 +6,7 @@ import (
"sync"
"time"
- "github.com/mailgun/timetools"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
// RTMetrics provides aggregated performance metrics for HTTP requests processing
@@ -24,29 +24,28 @@ type RTMetrics struct {
newCounter NewCounterFn
newHist NewRollingHistogramFn
- clock timetools.TimeProvider
}
type rrOptSetter func(r *RTMetrics) error
-// NewRTMetricsFn builder function type
+// NewRTMetricsFn builder function type.
type NewRTMetricsFn func() (*RTMetrics, error)
-// NewCounterFn builder function type
+// NewCounterFn builder function type.
type NewCounterFn func() (*RollingCounter, error)
-// NewRollingHistogramFn builder function type
+// NewRollingHistogramFn builder function type.
type NewRollingHistogramFn func() (*RollingHDRHistogram, error)
-// RTCounter set a builder function for Counter
-func RTCounter(new NewCounterFn) rrOptSetter {
+// RTCounter set a builder function for Counter.
+func RTCounter(fn NewCounterFn) rrOptSetter {
return func(r *RTMetrics) error {
- r.newCounter = new
+ r.newCounter = fn
return nil
}
}
-// RTHistogram set a builder function for RollingHistogram
+// RTHistogram set a builder function for RollingHistogram.
func RTHistogram(fn NewRollingHistogramFn) rrOptSetter {
return func(r *RTMetrics) error {
r.newHist = fn
@@ -54,14 +53,6 @@ func RTHistogram(fn NewRollingHistogramFn) rrOptSetter {
}
}
-// RTClock sets a clock
-func RTClock(clock timetools.TimeProvider) rrOptSetter {
- return func(r *RTMetrics) error {
- r.clock = clock
- return nil
- }
-}
-
// NewRTMetrics returns new instance of metrics collector.
func NewRTMetrics(settings ...rrOptSetter) (*RTMetrics, error) {
m := &RTMetrics{
@@ -74,19 +65,15 @@ func NewRTMetrics(settings ...rrOptSetter) (*RTMetrics, error) {
}
}
- if m.clock == nil {
- m.clock = &timetools.RealTime{}
- }
-
if m.newCounter == nil {
m.newCounter = func() (*RollingCounter, error) {
- return NewCounter(counterBuckets, counterResolution, CounterClock(m.clock))
+ return NewCounter(counterBuckets, counterResolution)
}
}
if m.newHist == nil {
m.newHist = func() (*RollingHDRHistogram, error) {
- return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets, RollingClock(m.clock))
+ return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets)
}
}
@@ -111,7 +98,7 @@ func NewRTMetrics(settings ...rrOptSetter) (*RTMetrics, error) {
return m, nil
}
-// Export Returns a new RTMetrics which is a copy of the current one
+// Export Returns a new RTMetrics which is a copy of the current one.
func (m *RTMetrics) Export() *RTMetrics {
m.statusCodesLock.RLock()
defer m.statusCodesLock.RUnlock()
@@ -133,12 +120,11 @@ func (m *RTMetrics) Export() *RTMetrics {
}
export.newCounter = m.newCounter
export.newHist = m.newHist
- export.clock = m.clock
return export
}
-// CounterWindowSize gets total windows size
+// CounterWindowSize gets total windows size.
func (m *RTMetrics) CounterWindowSize() time.Duration {
return m.total.WindowSize()
}
@@ -152,7 +138,7 @@ func (m *RTMetrics) NetworkErrorRatio() float64 {
return float64(m.netErrors.Count()) / float64(m.total.Count())
}
-// ResponseCodeRatio calculates ratio of count(startA to endA) / count(startB to endB)
+// ResponseCodeRatio calculates ratio of count(startA to endA) / count(startB to endB).
func (m *RTMetrics) ResponseCodeRatio(startA, endA, startB, endB int) float64 {
a := int64(0)
b := int64(0)
@@ -172,7 +158,7 @@ func (m *RTMetrics) ResponseCodeRatio(startA, endA, startB, endB int) float64 {
return 0
}
-// Append append a metric
+// Append append a metric.
func (m *RTMetrics) Append(other *RTMetrics) error {
if m == other {
return errors.New("RTMetrics cannot append to self")
@@ -206,14 +192,14 @@ func (m *RTMetrics) Append(other *RTMetrics) error {
return m.histogram.Append(copied.histogram)
}
-// Record records a metric
+// Record records a metric.
func (m *RTMetrics) Record(code int, duration time.Duration) {
m.total.Inc(1)
if code == http.StatusGatewayTimeout || code == http.StatusBadGateway {
m.netErrors.Inc(1)
}
- m.recordStatusCode(code)
- m.recordLatency(duration)
+ _ = m.recordStatusCode(code)
+ _ = m.recordLatency(duration)
}
// TotalCount returns total count of processed requests collected.
@@ -221,12 +207,12 @@ func (m *RTMetrics) TotalCount() int64 {
return m.total.Count()
}
-// NetworkErrorCount returns total count of processed requests observed
+// NetworkErrorCount returns total count of processed requests observed.
func (m *RTMetrics) NetworkErrorCount() int64 {
return m.netErrors.Count()
}
-// StatusCodesCounts returns map with counts of the response codes
+// StatusCodesCounts returns map with counts of the response codes.
func (m *RTMetrics) StatusCodesCounts() map[int]int64 {
sc := make(map[int]int64)
m.statusCodesLock.RLock()
@@ -246,7 +232,7 @@ func (m *RTMetrics) LatencyHistogram() (*HDRHistogram, error) {
return m.histogram.Merged()
}
-// Reset reset metrics
+// Reset reset metrics.
func (m *RTMetrics) Reset() {
m.statusCodesLock.Lock()
defer m.statusCodesLock.Unlock()
@@ -293,10 +279,10 @@ func (m *RTMetrics) recordStatusCode(statusCode int) error {
const (
counterBuckets = 10
- counterResolution = time.Second
+ counterResolution = clock.Second
histMin = 1
- histMax = 3600000000 // 1 hour in microseconds
- histSignificantFigures = 2 // significant figures (1% precision)
- histBuckets = 6 // number of sub-histograms in a rolling histogram
- histPeriod = 10 * time.Second // roll time
+ histMax = 3600000000 // 1 hour in microseconds
+ histSignificantFigures = 2 // significant figures (1% precision)
+ histBuckets = 6 // number of sub-histograms in a rolling histogram
+ histPeriod = 10 * clock.Second // roll time
)
diff --git a/memmetrics/roundtrip_test.go b/memmetrics/roundtrip_test.go
index 2de6be2..6009a75 100644
--- a/memmetrics/roundtrip_test.go
+++ b/memmetrics/roundtrip_test.go
@@ -6,21 +6,24 @@ import (
"testing"
"time"
- "github.com/mailgun/timetools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
func TestDefaults(t *testing.T) {
- rr, err := NewRTMetrics(RTClock(testutils.GetClock()))
+ done := testutils.FreezeTime()
+ defer done()
+
+ rr, err := NewRTMetrics()
require.NoError(t, err)
require.NotNil(t, rr)
- rr.Record(200, time.Second)
- rr.Record(502, 2*time.Second)
- rr.Record(200, time.Second)
- rr.Record(200, time.Second)
+ rr.Record(200, clock.Second)
+ rr.Record(502, 2*clock.Second)
+ rr.Record(200, clock.Second)
+ rr.Record(200, clock.Second)
assert.EqualValues(t, 1, rr.NetworkErrorCount())
assert.EqualValues(t, 4, rr.TotalCount())
@@ -30,7 +33,7 @@ func TestDefaults(t *testing.T) {
h, err := rr.LatencyHistogram()
require.NoError(t, err)
- assert.Equal(t, 2, int(h.LatencyAtQuantile(100)/time.Second))
+ assert.Equal(t, 2, int(h.LatencyAtQuantile(100)/clock.Second))
rr.Reset()
assert.EqualValues(t, 0, rr.NetworkErrorCount())
@@ -45,25 +48,26 @@ func TestDefaults(t *testing.T) {
}
func TestAppend(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- rr, err := NewRTMetrics(RTClock(clock))
+ rr, err := NewRTMetrics()
require.NoError(t, err)
require.NotNil(t, rr)
- rr.Record(200, time.Second)
- rr.Record(502, 2*time.Second)
- rr.Record(200, time.Second)
- rr.Record(200, time.Second)
+ rr.Record(200, clock.Second)
+ rr.Record(502, 2*clock.Second)
+ rr.Record(200, clock.Second)
+ rr.Record(200, clock.Second)
- rr2, err := NewRTMetrics(RTClock(clock))
+ rr2, err := NewRTMetrics()
require.NoError(t, err)
require.NotNil(t, rr2)
- rr2.Record(200, 3*time.Second)
- rr2.Record(501, 3*time.Second)
- rr2.Record(200, 3*time.Second)
- rr2.Record(200, 3*time.Second)
+ rr2.Record(200, 3*clock.Second)
+ rr2.Record(501, 3*clock.Second)
+ rr2.Record(200, 3*clock.Second)
+ rr2.Record(200, 3*clock.Second)
require.NoError(t, rr2.Append(rr))
assert.Equal(t, map[int]int64{501: 1, 502: 1, 200: 6}, rr2.StatusCodesCounts())
@@ -71,14 +75,14 @@ func TestAppend(t *testing.T) {
h, err := rr2.LatencyHistogram()
require.NoError(t, err)
- assert.EqualValues(t, 3, h.LatencyAtQuantile(100)/time.Second)
+ assert.EqualValues(t, 3, h.LatencyAtQuantile(100)/clock.Second)
}
func TestConcurrentRecords(t *testing.T) {
// This test asserts a race condition which requires parallelism
runtime.GOMAXPROCS(100)
- rr, err := NewRTMetrics(RTClock(testutils.GetClock()))
+ rr, err := NewRTMetrics()
require.NoError(t, err)
for code := 0; code < 100; code++ {
@@ -92,7 +96,6 @@ func TestConcurrentRecords(t *testing.T) {
func TestRTMetricExportReturnsNewCopy(t *testing.T) {
a := RTMetrics{
- clock: &timetools.RealTime{},
statusCodes: map[int]*RollingCounter{},
statusCodesLock: sync.RWMutex{},
histogram: &RollingHDRHistogram{},
@@ -100,17 +103,17 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) {
}
var err error
- a.total, err = NewCounter(1, time.Second, CounterClock(a.clock))
+ a.total, err = NewCounter(1, clock.Second)
require.NoError(t, err)
- a.netErrors, err = NewCounter(1, time.Second, CounterClock(a.clock))
+ a.netErrors, err = NewCounter(1, clock.Second)
require.NoError(t, err)
a.newCounter = func() (*RollingCounter, error) {
- return NewCounter(counterBuckets, counterResolution, CounterClock(a.clock))
+ return NewCounter(counterBuckets, counterResolution)
}
a.newHist = func() (*RollingHDRHistogram, error) {
- return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets, RollingClock(a.clock))
+ return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets)
}
b := a.Export()
@@ -120,7 +123,6 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) {
a.histogram = nil
a.newCounter = nil
a.newHist = nil
- a.clock = nil
assert.NotNil(t, b.total)
assert.NotNil(t, b.netErrors)
@@ -128,7 +130,6 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) {
assert.NotNil(t, b.histogram)
assert.NotNil(t, b.newCounter)
assert.NotNil(t, b.newHist)
- assert.NotNil(t, b.clock)
// a and b should have different locks
locksSucceed := make(chan bool)
@@ -144,7 +145,7 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) {
select {
case <-locksSucceed:
return
- case <-time.After(10 * time.Second):
+ case <-clock.After(10 * clock.Second):
t.FailNow()
}
}
diff --git a/ratelimit/bucket.go b/ratelimit/bucket.go
index 4f8416d..9d81c0a 100644
--- a/ratelimit/bucket.go
+++ b/ratelimit/bucket.go
@@ -4,10 +4,10 @@ import (
"fmt"
"time"
- "github.com/mailgun/timetools"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
-// UndefinedDelay default delay
+// UndefinedDelay default delay.
const UndefinedDelay = -1
// rate defines token bucket parameters.
@@ -34,27 +34,24 @@ type tokenBucket struct {
// The number of tokens available for consumption at the moment. It can
// nether be larger then capacity.
availableTokens int64
- // Interface that gives current time (so tests can override)
- clock timetools.TimeProvider
// Tells when tokensAvailable was updated the last time.
- lastRefresh time.Time
+ lastRefresh clock.Time
// The number of tokens consumed the last time.
lastConsumed int64
}
// newTokenBucket crates a `tokenBucket` instance for the specified `Rate`.
-func newTokenBucket(rate *rate, clock timetools.TimeProvider) *tokenBucket {
+func newTokenBucket(rate *rate) *tokenBucket {
period := rate.period
if period == 0 {
- period = time.Nanosecond
+ period = clock.Nanosecond
}
return &tokenBucket{
period: period,
timePerToken: time.Duration(int64(period) / rate.average),
burst: rate.burst,
- clock: clock,
- lastRefresh: clock.UtcNow(),
+ lastRefresh: clock.Now().UTC(),
availableTokens: rate.burst,
}
}
@@ -90,7 +87,7 @@ func (tb *tokenBucket) rollback() {
}
// update modifies `average` and `burst` fields of the token bucket according
-// to the provided `Rate`
+// to the provided `Rate`.
func (tb *tokenBucket) update(rate *rate) error {
if rate.period != tb.period {
return fmt.Errorf("period mismatch: %v != %v", tb.period, rate.period)
@@ -114,7 +111,7 @@ func (tb *tokenBucket) timeTillAvailable(tokens int64) time.Duration {
// It is calculated based on the refill rate, the time passed since last refresh,
// and is limited by the bucket capacity.
func (tb *tokenBucket) updateAvailableTokens() {
- now := tb.clock.UtcNow()
+ now := clock.Now().UTC()
timePassed := now.Sub(tb.lastRefresh)
if tb.timePerToken == 0 {
diff --git a/ratelimit/bucket_test.go b/ratelimit/bucket_test.go
index 29d56b9..1d2d76c 100644
--- a/ratelimit/bucket_test.go
+++ b/ratelimit/bucket_test.go
@@ -4,16 +4,17 @@ import (
"testing"
"time"
- "github.com/mailgun/timetools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
func TestConsumeSingleToken(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tb := newTokenBucket(&rate{period: time.Second, average: 1, burst: 1}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 1, burst: 1})
// First request passes
delay, err := tb.consume(1)
@@ -23,10 +24,10 @@ func TestConsumeSingleToken(t *testing.T) {
// Next request does not pass the same second
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, time.Second, delay)
+ assert.Equal(t, clock.Second, delay)
// Second later, the request passes
- clock.Sleep(time.Second)
+ clock.Advance(clock.Second)
delay, err = tb.consume(1)
require.NoError(t, err)
@@ -34,7 +35,7 @@ func TestConsumeSingleToken(t *testing.T) {
// Five seconds later, still only one request is allowed
// because maxBurst is 1
- clock.Sleep(5 * time.Second)
+ clock.Advance(5 * clock.Second)
delay, err = tb.consume(1)
require.NoError(t, err)
@@ -43,13 +44,14 @@ func TestConsumeSingleToken(t *testing.T) {
// The next one is forbidden
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, time.Second, delay)
+ assert.Equal(t, clock.Second, delay)
}
func TestFastConsumption(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tb := newTokenBucket(&rate{period: time.Second, average: 1, burst: 1}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 1, burst: 1})
// First request passes
delay, err := tb.consume(1)
@@ -57,21 +59,21 @@ func TestFastConsumption(t *testing.T) {
assert.Equal(t, time.Duration(0), delay)
// Try 200 ms later
- clock.Sleep(time.Millisecond * 200)
+ clock.Advance(clock.Millisecond * 200)
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, time.Second, delay)
+ assert.Equal(t, clock.Second, delay)
// Try 700 ms later
- clock.Sleep(time.Millisecond * 700)
+ clock.Advance(clock.Millisecond * 700)
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, time.Second, delay)
+ assert.Equal(t, clock.Second, delay)
// Try 100 ms later, success!
- clock.Sleep(time.Millisecond * 100)
+ clock.Advance(clock.Millisecond * 100)
delay, err = tb.consume(1)
require.NoError(t, err)
@@ -79,7 +81,10 @@ func TestFastConsumption(t *testing.T) {
}
func TestConsumeMultipleTokens(t *testing.T) {
- tb := newTokenBucket(&rate{period: time.Second, average: 3, burst: 5}, testutils.GetClock())
+ done := testutils.FreezeTime()
+ defer done()
+
+ tb := newTokenBucket(&rate{period: clock.Second, average: 3, burst: 5})
delay, err := tb.consume(3)
require.NoError(t, err)
@@ -95,9 +100,10 @@ func TestConsumeMultipleTokens(t *testing.T) {
}
func TestDelayIsCorrect(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tb := newTokenBucket(&rate{period: time.Second, average: 3, burst: 5}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 3, burst: 5})
// Exhaust initial capacity
delay, err := tb.consume(5)
@@ -109,26 +115,32 @@ func TestDelayIsCorrect(t *testing.T) {
assert.NotEqual(t, time.Duration(0), delay)
// Now wait provided delay and make sure we can consume now
- clock.Sleep(delay)
+ clock.Advance(delay)
delay, err = tb.consume(3)
require.NoError(t, err)
assert.Equal(t, time.Duration(0), delay)
}
-// Make sure requests that exceed burst size are not allowed
+// Make sure requests that exceed burst size are not allowed.
func TestExceedsBurst(t *testing.T) {
- tb := newTokenBucket(&rate{period: time.Second, average: 1, burst: 10}, testutils.GetClock())
+ done := testutils.FreezeTime()
+ defer done()
+
+ tb := newTokenBucket(&rate{period: clock.Second, average: 1, burst: 10})
_, err := tb.consume(11)
require.Error(t, err)
}
func TestConsumeBurst(t *testing.T) {
- tb := newTokenBucket(&rate{period: time.Second, average: 2, burst: 5}, testutils.GetClock())
+ done := testutils.FreezeTime()
+ defer done()
+
+ tb := newTokenBucket(&rate{period: clock.Second, average: 2, burst: 5})
// In two seconds we would have 5 tokens
- testutils.GetClock().Sleep(2 * time.Second)
+ clock.Advance(2 * clock.Second)
// Lets consume 5 at once
delay, err := tb.consume(5)
@@ -137,7 +149,10 @@ func TestConsumeBurst(t *testing.T) {
}
func TestConsumeEstimate(t *testing.T) {
- tb := newTokenBucket(&rate{period: time.Second, average: 2, burst: 4}, testutils.GetClock())
+ done := testutils.FreezeTime()
+ defer done()
+
+ tb := newTokenBucket(&rate{period: clock.Second, average: 2, burst: 4})
// Consume all burst at once
delay, err := tb.consume(4)
@@ -147,31 +162,32 @@ func TestConsumeEstimate(t *testing.T) {
// Now try to consume it and face delay
delay, err = tb.consume(4)
require.NoError(t, err)
- assert.Equal(t, time.Duration(2)*time.Second, delay)
+ assert.Equal(t, time.Duration(2)*clock.Second, delay)
}
// If a rate with different period is passed to the `update` method, then an
// error is returned but the state of the bucket remains valid and unchanged.
func TestUpdateInvalidPeriod(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(15) // 5 tokens available
require.NoError(t, err)
// When
- err = tb.update(&rate{period: time.Second + 1, average: 30, burst: 40}) // still 5 tokens available
+ err = tb.update(&rate{period: clock.Second + 1, average: 30, burst: 40}) // still 5 tokens available
require.Error(t, err)
// Then
// ...check that rate did not change
- clock.Sleep(500 * time.Millisecond)
+ clock.Advance(500 * clock.Millisecond)
delay, err := tb.consume(11)
require.NoError(t, err)
- assert.Equal(t, 100*time.Millisecond, delay)
+ assert.Equal(t, 100*clock.Millisecond, delay)
delay, err = tb.consume(10)
require.NoError(t, err)
@@ -179,7 +195,7 @@ func TestUpdateInvalidPeriod(t *testing.T) {
assert.Equal(t, time.Duration(0), delay)
// ...check that burst did not change
- clock.Sleep(40 * time.Second)
+ clock.Advance(40 * clock.Second)
_, err = tb.consume(21)
require.Error(t, err)
@@ -192,35 +208,37 @@ func TestUpdateInvalidPeriod(t *testing.T) {
// If the capacity of the bucket is increased by the update then it takes some
// time to fill the bucket with tokens up to the new capacity.
func TestUpdateBurstIncreased(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(15) // 5 tokens available
require.NoError(t, err)
// When
- err = tb.update(&rate{period: time.Second, average: 10, burst: 50}) // still 5 tokens available
+ err = tb.update(&rate{period: clock.Second, average: 10, burst: 50}) // still 5 tokens available
require.NoError(t, err)
// Then
delay, err := tb.consume(50)
require.NoError(t, err)
- assert.Equal(t, time.Duration(time.Second/10*45), delay)
+ assert.Equal(t, clock.Second/10*45, delay)
}
// If the capacity of the bucket is increased by the update then it takes some
// time to fill the bucket with tokens up to the new capacity.
func TestUpdateBurstDecreased(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 50}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 50})
_, err := tb.consume(15) // 35 tokens available
require.NoError(t, err)
// When
- err = tb.update(&rate{period: time.Second, average: 10, burst: 20}) // the number of available tokens reduced to 20.
+ err = tb.update(&rate{period: clock.Second, average: 10, burst: 20}) // the number of available tokens reduced to 20.
require.NoError(t, err)
// Then
@@ -231,29 +249,31 @@ func TestUpdateBurstDecreased(t *testing.T) {
// If rate is updated then it affects the bucket refill speed.
func TestUpdateRateChanged(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(15) // 5 tokens available
require.NoError(t, err)
// When
- err = tb.update(&rate{period: time.Second, average: 20, burst: 20}) // still 5 tokens available
+ err = tb.update(&rate{period: clock.Second, average: 20, burst: 20}) // still 5 tokens available
require.NoError(t, err)
// Then
delay, err := tb.consume(20)
require.NoError(t, err)
- assert.Equal(t, time.Duration(time.Second/20*15), delay)
+ assert.Equal(t, clock.Second/20*15, delay)
}
// Only the most recent consumption is reverted by `Rollback`.
func TestRollback(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(8) // 12 tokens available
require.NoError(t, err)
_, err = tb.consume(7) // 5 tokens available
@@ -269,14 +289,17 @@ func TestRollback(t *testing.T) {
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, 100*time.Millisecond, delay)
+ assert.Equal(t, 100*clock.Millisecond, delay)
}
// It is safe to call `Rollback` several times. The second and all subsequent
// calls just do nothing.
func TestRollbackSeveralTimes(t *testing.T) {
+ done := testutils.FreezeTime()
+ defer done()
+
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, testutils.GetClock())
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(8) // 12 tokens available
require.NoError(t, err)
tb.rollback() // 20 tokens available
@@ -293,19 +316,22 @@ func TestRollbackSeveralTimes(t *testing.T) {
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, 100*time.Millisecond, delay)
+ assert.Equal(t, 100*clock.Millisecond, delay)
}
// If previous consumption returned a delay due to an attempt to consume more
// tokens then there are available, then `Rollback` has no effect.
func TestRollbackAfterAvailableExceeded(t *testing.T) {
+ done := testutils.FreezeTime()
+ defer done()
+
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, testutils.GetClock())
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(8) // 12 tokens available
require.NoError(t, err)
delay, err := tb.consume(15) // still 12 tokens available
require.NoError(t, err)
- assert.Equal(t, 300*time.Millisecond, delay)
+ assert.Equal(t, 300*clock.Millisecond, delay)
// When
tb.rollback() // Previous operation consumed 0 tokens, so rollback has no effect.
@@ -317,16 +343,17 @@ func TestRollbackAfterAvailableExceeded(t *testing.T) {
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, 100*time.Millisecond, delay)
+ assert.Equal(t, 100*clock.Millisecond, delay)
}
// If previous consumption returned a error due to an attempt to consume more
// tokens then the bucket's burst size, then `Rollback` has no effect.
func TestRollbackAfterError(t *testing.T) {
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// Given
- tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock)
+ tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20})
_, err := tb.consume(8) // 12 tokens available
require.NoError(t, err)
delay, err := tb.consume(21) // still 12 tokens available
@@ -343,18 +370,16 @@ func TestRollbackAfterError(t *testing.T) {
delay, err = tb.consume(1)
require.NoError(t, err)
- assert.Equal(t, 100*time.Millisecond, delay)
+ assert.Equal(t, 100*clock.Millisecond, delay)
}
func TestDivisionByZeroOnPeriod(t *testing.T) {
- clock := &timetools.RealTime{}
-
var emptyPeriod int64
- tb := newTokenBucket(&rate{period: time.Duration(emptyPeriod), average: 2, burst: 2}, clock)
+ tb := newTokenBucket(&rate{period: time.Duration(emptyPeriod), average: 2, burst: 2})
_, err := tb.consume(1)
assert.NoError(t, err)
- err = tb.update(&rate{period: time.Nanosecond, average: 1, burst: 1})
+ err = tb.update(&rate{period: clock.Nanosecond, average: 1, burst: 1})
assert.NoError(t, err)
}
diff --git a/ratelimit/bucketset.go b/ratelimit/bucketset.go
index af2c8bb..b2f5b20 100644
--- a/ratelimit/bucketset.go
+++ b/ratelimit/bucketset.go
@@ -5,25 +5,21 @@ import (
"sort"
"strings"
"time"
-
- "github.com/mailgun/timetools"
)
// TokenBucketSet represents a set of TokenBucket covering different time periods.
type TokenBucketSet struct {
buckets map[time.Duration]*tokenBucket
maxPeriod time.Duration
- clock timetools.TimeProvider
}
// NewTokenBucketSet creates a `TokenBucketSet` from the specified `rates`.
-func NewTokenBucketSet(rates *RateSet, clock timetools.TimeProvider) *TokenBucketSet {
+func NewTokenBucketSet(rates *RateSet) *TokenBucketSet {
tbs := new(TokenBucketSet)
- tbs.clock = clock
// In the majority of cases we will have only one bucket.
tbs.buckets = make(map[time.Duration]*tokenBucket, len(rates.m))
for _, rate := range rates.m {
- newBucket := newTokenBucket(rate, clock)
+ newBucket := newTokenBucket(rate)
tbs.buckets[rate.period] = newBucket
tbs.maxPeriod = maxDuration(tbs.maxPeriod, rate.period)
}
@@ -35,7 +31,7 @@ func (tbs *TokenBucketSet) Update(rates *RateSet) {
// Update existing buckets and delete those that have no corresponding spec.
for _, bucket := range tbs.buckets {
if rate, ok := rates.m[bucket.period]; ok {
- bucket.update(rate)
+ _ = bucket.update(rate)
} else {
delete(tbs.buckets, bucket.period)
}
@@ -43,7 +39,7 @@ func (tbs *TokenBucketSet) Update(rates *RateSet) {
// Add missing buckets.
for _, rate := range rates.m {
if _, ok := tbs.buckets[rate.period]; !ok {
- newBucket := newTokenBucket(rate, tbs.clock)
+ newBucket := newTokenBucket(rate)
tbs.buckets[rate.period] = newBucket
}
}
@@ -54,7 +50,7 @@ func (tbs *TokenBucketSet) Update(rates *RateSet) {
}
}
-// Consume consume tokens
+// Consume consume tokens.
func (tbs *TokenBucketSet) Consume(tokens int64) (time.Duration, error) {
var maxDelay time.Duration = UndefinedDelay
var firstErr error
@@ -81,7 +77,7 @@ func (tbs *TokenBucketSet) Consume(tokens int64) (time.Duration, error) {
return maxDelay, firstErr
}
-// GetMaxPeriod returns the max period
+// GetMaxPeriod returns the max period.
func (tbs *TokenBucketSet) GetMaxPeriod() time.Duration {
return tbs.maxPeriod
}
@@ -89,11 +85,11 @@ func (tbs *TokenBucketSet) GetMaxPeriod() time.Duration {
// debugState returns string that reflects the current state of all buckets in
// this set. It is intended to be used for debugging and testing only.
func (tbs *TokenBucketSet) debugState() string {
- periods := sort.IntSlice(make([]int, 0, len(tbs.buckets)))
+ periods := make([]int64, 0, len(tbs.buckets))
for period := range tbs.buckets {
- periods = append(periods, int(period))
+ periods = append(periods, int64(period))
}
- sort.Sort(periods)
+ sort.Slice(periods, func(i, j int) bool { return periods[i] < periods[j] })
bucketRepr := make([]string, 0, len(tbs.buckets))
for _, period := range periods {
bucket := tbs.buckets[time.Duration(period)]
@@ -102,7 +98,7 @@ func (tbs *TokenBucketSet) debugState() string {
return strings.Join(bucketRepr, ", ")
}
-func maxDuration(x time.Duration, y time.Duration) time.Duration {
+func maxDuration(x, y time.Duration) time.Duration {
if x > y {
return x
}
diff --git a/ratelimit/bucketset_test.go b/ratelimit/bucketset_test.go
index fd76319..613b637 100644
--- a/ratelimit/bucketset_test.go
+++ b/ratelimit/bucketset_test.go
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
@@ -13,29 +14,31 @@ import (
func TestLongestPeriod(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(1*time.Second, 10, 20))
- require.NoError(t, rates.Add(7*time.Second, 10, 20))
- require.NoError(t, rates.Add(5*time.Second, 11, 21))
+ require.NoError(t, rates.Add(1*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(7*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(5*clock.Second, 11, 21))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
// When
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
// Then
- assert.Equal(t, 7*time.Second, tbs.maxPeriod)
+ assert.Equal(t, 7*clock.Second, tbs.maxPeriod)
}
// Successful token consumption updates state of all buckets in the set.
func TestConsume(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(1*time.Second, 10, 20))
- require.NoError(t, rates.Add(10*time.Second, 20, 50))
+ require.NoError(t, rates.Add(1*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(10*clock.Second, 20, 50))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
// When
delay, err := tbs.Consume(15)
@@ -50,19 +53,20 @@ func TestConsume(t *testing.T) {
func TestConsumeRefill(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(10*time.Second, 10, 20))
- require.NoError(t, rates.Add(100*time.Second, 20, 50))
+ require.NoError(t, rates.Add(10*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(100*clock.Second, 20, 50))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(15)
require.NoError(t, err)
assert.Equal(t, "{10s: 5}, {1m40s: 35}", tbs.debugState())
// When
- clock.Sleep(10 * time.Second)
+ clock.Advance(10 * clock.Second)
delay, err := tbs.Consume(0) // Consumes nothing but forces an internal state update.
require.NoError(t, err)
@@ -77,12 +81,13 @@ func TestConsumeRefill(t *testing.T) {
func TestConsumeLimitedBy1st(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(10*time.Second, 10, 10))
- require.NoError(t, rates.Add(100*time.Second, 20, 20))
+ require.NoError(t, rates.Add(10*clock.Second, 10, 10))
+ require.NoError(t, rates.Add(100*clock.Second, 20, 20))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(5)
require.NoError(t, err)
@@ -93,7 +98,7 @@ func TestConsumeLimitedBy1st(t *testing.T) {
require.NoError(t, err)
// Then
- assert.Equal(t, 5*time.Second, delay)
+ assert.Equal(t, 5*clock.Second, delay)
assert.Equal(t, "{10s: 5}, {1m40s: 15}", tbs.debugState())
}
@@ -102,22 +107,23 @@ func TestConsumeLimitedBy1st(t *testing.T) {
func TestConsumeLimitedBy2st(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(10*time.Second, 10, 10))
- require.NoError(t, rates.Add(100*time.Second, 20, 20))
+ require.NoError(t, rates.Add(10*clock.Second, 10, 10))
+ require.NoError(t, rates.Add(100*clock.Second, 20, 20))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(10)
require.NoError(t, err)
- clock.Sleep(10 * time.Second)
+ clock.Advance(10 * clock.Second)
_, err = tbs.Consume(10)
require.NoError(t, err)
- clock.Sleep(5 * time.Second)
+ clock.Advance(5 * clock.Second)
_, err = tbs.Consume(0)
require.NoError(t, err)
@@ -128,7 +134,7 @@ func TestConsumeLimitedBy2st(t *testing.T) {
require.NoError(t, err)
// Then
- assert.Equal(t, 7*(5*time.Second), delay)
+ assert.Equal(t, 7*(5*clock.Second), delay)
assert.Equal(t, "{10s: 5}, {1m40s: 3}", tbs.debugState())
}
@@ -137,12 +143,13 @@ func TestConsumeLimitedBy2st(t *testing.T) {
func TestConsumeMoreThenBurst(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(1*time.Second, 10, 20))
- require.NoError(t, rates.Add(10*time.Second, 50, 100))
+ require.NoError(t, rates.Add(1*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(10*clock.Second, 50, 100))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(5)
require.NoError(t, err)
@@ -160,84 +167,87 @@ func TestConsumeMoreThenBurst(t *testing.T) {
func TestUpdateMore(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(1*time.Second, 10, 20))
- require.NoError(t, rates.Add(10*time.Second, 20, 50))
- require.NoError(t, rates.Add(20*time.Second, 45, 90))
+ require.NoError(t, rates.Add(1*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(10*clock.Second, 20, 50))
+ require.NoError(t, rates.Add(20*clock.Second, 45, 90))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(5)
require.NoError(t, err)
assert.Equal(t, "{1s: 15}, {10s: 45}, {20s: 85}", tbs.debugState())
rates = NewRateSet()
- require.NoError(t, rates.Add(10*time.Second, 30, 40))
- require.NoError(t, rates.Add(11*time.Second, 30, 40))
- require.NoError(t, rates.Add(12*time.Second, 30, 40))
- require.NoError(t, rates.Add(13*time.Second, 30, 40))
+ require.NoError(t, rates.Add(10*clock.Second, 30, 40))
+ require.NoError(t, rates.Add(11*clock.Second, 30, 40))
+ require.NoError(t, rates.Add(12*clock.Second, 30, 40))
+ require.NoError(t, rates.Add(13*clock.Second, 30, 40))
// When
tbs.Update(rates)
// Then
assert.Equal(t, "{10s: 40}, {11s: 40}, {12s: 40}, {13s: 40}", tbs.debugState())
- assert.Equal(t, 13*time.Second, tbs.maxPeriod)
+ assert.Equal(t, 13*clock.Second, tbs.maxPeriod)
}
// Update operation can remove buckets.
func TestUpdateLess(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(1*time.Second, 10, 20))
- require.NoError(t, rates.Add(10*time.Second, 20, 50))
- require.NoError(t, rates.Add(20*time.Second, 45, 90))
- require.NoError(t, rates.Add(30*time.Second, 50, 100))
+ require.NoError(t, rates.Add(1*clock.Second, 10, 20))
+ require.NoError(t, rates.Add(10*clock.Second, 20, 50))
+ require.NoError(t, rates.Add(20*clock.Second, 45, 90))
+ require.NoError(t, rates.Add(30*clock.Second, 50, 100))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(5)
require.NoError(t, err)
assert.Equal(t, "{1s: 15}, {10s: 45}, {20s: 85}, {30s: 95}", tbs.debugState())
rates = NewRateSet()
- require.NoError(t, rates.Add(10*time.Second, 25, 20))
- require.NoError(t, rates.Add(20*time.Second, 30, 21))
+ require.NoError(t, rates.Add(10*clock.Second, 25, 20))
+ require.NoError(t, rates.Add(20*clock.Second, 30, 21))
// When
tbs.Update(rates)
// Then
assert.Equal(t, "{10s: 20}, {20s: 21}", tbs.debugState())
- assert.Equal(t, 20*time.Second, tbs.maxPeriod)
+ assert.Equal(t, 20*clock.Second, tbs.maxPeriod)
}
// Update operation can remove buckets.
func TestUpdateAllDifferent(t *testing.T) {
// Given
rates := NewRateSet()
- require.NoError(t, rates.Add(10*time.Second, 20, 50))
- require.NoError(t, rates.Add(30*time.Second, 50, 100))
+ require.NoError(t, rates.Add(10*clock.Second, 20, 50))
+ require.NoError(t, rates.Add(30*clock.Second, 50, 100))
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tbs := NewTokenBucketSet(rates, clock)
+ tbs := NewTokenBucketSet(rates)
_, err := tbs.Consume(5)
require.NoError(t, err)
assert.Equal(t, "{10s: 45}, {30s: 95}", tbs.debugState())
rates = NewRateSet()
- require.NoError(t, rates.Add(1*time.Second, 10, 40))
- require.NoError(t, rates.Add(60*time.Second, 100, 150))
+ require.NoError(t, rates.Add(1*clock.Second, 10, 40))
+ require.NoError(t, rates.Add(60*clock.Second, 100, 150))
// When
tbs.Update(rates)
// Then
assert.Equal(t, "{1s: 40}, {1m0s: 150}", tbs.debugState())
- assert.Equal(t, 60*time.Second, tbs.maxPeriod)
+ assert.Equal(t, 60*clock.Second, tbs.maxPeriod)
}
diff --git a/ratelimit/tokenlimiter.go b/ratelimit/tokenlimiter.go
index 774bf93..023cff1 100644
--- a/ratelimit/tokenlimiter.go
+++ b/ratelimit/tokenlimiter.go
@@ -7,13 +7,13 @@ import (
"sync"
"time"
- "github.com/mailgun/timetools"
- "github.com/mailgun/ttlmap"
log "github.com/sirupsen/logrus"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
+ "github.com/vulcand/oxy/internal/holsterv4/collections"
"github.com/vulcand/oxy/utils"
)
-// DefaultCapacity default capacity
+// DefaultCapacity default capacity.
const DefaultCapacity = 65536
// RateSet maintains a set of rates. It can contain only one rate per period at a time.
@@ -48,15 +48,15 @@ func (rs *RateSet) String() string {
return fmt.Sprint(rs.m)
}
-// RateExtractor rate extractor
+// RateExtractor rate extractor.
type RateExtractor interface {
Extract(r *http.Request) (*RateSet, error)
}
-// RateExtractorFunc rate extractor function type
+// RateExtractorFunc rate extractor function type.
type RateExtractorFunc func(r *http.Request) (*RateSet, error)
-// Extract extract from request
+// Extract extract from request.
func (e RateExtractorFunc) Extract(r *http.Request) (*RateSet, error) {
return e(r)
}
@@ -66,9 +66,8 @@ type TokenLimiter struct {
defaultRates *RateSet
extract utils.SourceExtractor
extractRates RateExtractor
- clock timetools.TimeProvider
mutex sync.Mutex
- bucketSets *ttlmap.TtlMap
+ bucketSets *collections.TTLMap
errHandler utils.ErrorHandler
capacity int
next http.Handler
@@ -98,11 +97,7 @@ func New(next http.Handler, extract utils.SourceExtractor, defaultRates *RateSet
}
}
setDefaults(tl)
- bucketSets, err := ttlmap.NewMapWithProvider(tl.capacity, tl.clock)
- if err != nil {
- return nil, err
- }
- tl.bucketSets = bucketSets
+ tl.bucketSets = collections.NewTTLMap(tl.capacity)
return tl, nil
}
@@ -149,10 +144,13 @@ func (tl *TokenLimiter) consumeRates(req *http.Request, source string, amount in
bucketSet = bucketSetI.(*TokenBucketSet)
bucketSet.Update(effectiveRates)
} else {
- bucketSet = NewTokenBucketSet(effectiveRates, tl.clock)
+ bucketSet = NewTokenBucketSet(effectiveRates)
// We set ttl as 10 times rate period. E.g. if rate is 100 requests/second per client ip
// the counters for this ip will expire after 10 seconds of inactivity
- tl.bucketSets.Set(source, bucketSet, int(bucketSet.maxPeriod/time.Second)*10+1)
+ err := tl.bucketSets.Set(source, bucketSet, int(bucketSet.maxPeriod/clock.Second)*10+1)
+ if err != nil {
+ return err
+ }
}
delay, err := bucketSet.Consume(amount)
if err != nil {
@@ -186,7 +184,7 @@ func (tl *TokenLimiter) resolveRates(req *http.Request) *RateSet {
return rates
}
-// MaxRateError max rate error
+// MaxRateError max rate error.
type MaxRateError struct {
Delay time.Duration
}
@@ -195,7 +193,7 @@ func (m *MaxRateError) Error() string {
return fmt.Sprintf("max rate reached: retry-in %v", m.Delay)
}
-// RateErrHandler error handler
+// RateErrHandler error handler.
type RateErrHandler struct{}
func (e *RateErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) {
@@ -203,16 +201,16 @@ func (e *RateErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err
w.Header().Set("Retry-After", fmt.Sprintf("%.0f", rerr.Delay.Seconds()))
w.Header().Set("X-Retry-In", rerr.Delay.String())
w.WriteHeader(http.StatusTooManyRequests)
- w.Write([]byte(err.Error()))
+ _, _ = w.Write([]byte(err.Error()))
return
}
utils.DefaultHandler.ServeHTTP(w, req, err)
}
-// TokenLimiterOption token limiter option type
+// TokenLimiterOption token limiter option type.
type TokenLimiterOption func(l *TokenLimiter) error
-// ErrorHandler sets error handler of the server
+// ErrorHandler sets error handler of the server.
func ErrorHandler(h utils.ErrorHandler) TokenLimiterOption {
return func(cl *TokenLimiter) error {
cl.errHandler = h
@@ -220,7 +218,7 @@ func ErrorHandler(h utils.ErrorHandler) TokenLimiterOption {
}
}
-// ExtractRates sets the rate extractor
+// ExtractRates sets the rate extractor.
func ExtractRates(e RateExtractor) TokenLimiterOption {
return func(cl *TokenLimiter) error {
cl.extractRates = e
@@ -228,21 +226,13 @@ func ExtractRates(e RateExtractor) TokenLimiterOption {
}
}
-// Clock sets the clock
-func Clock(clock timetools.TimeProvider) TokenLimiterOption {
+// Capacity sets the capacity.
+func Capacity(capacity int) TokenLimiterOption {
return func(cl *TokenLimiter) error {
- cl.clock = clock
- return nil
- }
-}
-
-// Capacity sets the capacity
-func Capacity(cap int) TokenLimiterOption {
- return func(cl *TokenLimiter) error {
- if cap <= 0 {
- return fmt.Errorf("bad capacity: %v", cap)
+ if capacity <= 0 {
+ return fmt.Errorf("bad capacity: %v", capacity)
}
- cl.capacity = cap
+ cl.capacity = capacity
return nil
}
}
@@ -253,9 +243,6 @@ func setDefaults(tl *TokenLimiter) {
if tl.capacity <= 0 {
tl.capacity = DefaultCapacity
}
- if tl.clock == nil {
- tl.clock = &timetools.RealTime{}
- }
if tl.errHandler == nil {
tl.errHandler = defaultErrHandler
}
diff --git a/ratelimit/tokenlimiter_test.go b/ratelimit/tokenlimiter_test.go
index f4319a6..c6f866e 100644
--- a/ratelimit/tokenlimiter_test.go
+++ b/ratelimit/tokenlimiter_test.go
@@ -5,10 +5,10 @@ import (
"net/http"
"net/http/httptest"
"testing"
- "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
"github.com/vulcand/oxy/utils"
)
@@ -21,31 +21,32 @@ func TestRateSetAdd(t *testing.T) {
require.Error(t, err)
// Invalid Average
- err = rs.Add(time.Second, 0, 1)
+ err = rs.Add(clock.Second, 0, 1)
require.Error(t, err)
// Invalid Burst
- err = rs.Add(time.Second, 1, 0)
+ err = rs.Add(clock.Second, 1, 0)
require.Error(t, err)
- err = rs.Add(time.Second, 1, 1)
+ err = rs.Add(clock.Second, 1, 1)
require.NoError(t, err)
- assert.Equal(t, fmt.Sprint(rs), "map[1s:rate(1/1s, burst=1)]")
+ assert.Equal(t, rs.String(), "map[1s:rate(1/1s, burst=1)]")
}
-// We've hit the limit and were able to proceed on the next time run
+// We've hit the limit and were able to proceed on the next time run.
func TestHitLimit(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, headerLimit, rates, Clock(clock))
+ l, err := New(handler, headerLimit, rates)
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -61,25 +62,26 @@ func TestHitLimit(t *testing.T) {
assert.Equal(t, 429, re.StatusCode)
// Second later, the request from this ip will succeed
- clock.Sleep(time.Second)
+ clock.Advance(clock.Second)
re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a"))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
}
-// We've failed to extract client ip
+// We've failed to extract client ip.
func TestFailure(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, faultyExtract, rates, Clock(clock))
+ l, err := New(handler, faultyExtract, rates)
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -90,19 +92,20 @@ func TestFailure(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, re.StatusCode)
}
-// Make sure rates from different ips are controlled separately
+// Make sure rates from different ips are controlled separately.
func TestIsolation(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, headerLimit, rates, Clock(clock))
+ l, err := New(handler, headerLimit, rates)
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -123,19 +126,20 @@ func TestIsolation(t *testing.T) {
assert.Equal(t, http.StatusOK, re.StatusCode)
}
-// Make sure that expiration works (Expiration is triggered after significant amount of time passes)
+// Make sure that expiration works (Expiration is triggered after significant amount of time passes).
func TestExpiration(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, headerLimit, rates, Clock(clock))
+ l, err := New(handler, headerLimit, rates)
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -151,7 +155,7 @@ func TestExpiration(t *testing.T) {
assert.Equal(t, 429, re.StatusCode)
// 24 hours later, the request from this ip will succeed
- clock.Sleep(24 * time.Hour)
+ clock.Advance(24 * clock.Hour)
re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a"))
require.NoError(t, err)
@@ -163,11 +167,11 @@ func TestExtractRates(t *testing.T) {
// Given
extractRates := func(*http.Request) (*RateSet, error) {
rates := NewRateSet()
- err := rates.Add(time.Second, 2, 2)
+ err := rates.Add(clock.Second, 2, 2)
if err != nil {
return nil, err
}
- err = rates.Add(60*time.Second, 10, 10)
+ err = rates.Add(60*clock.Second, 10, 10)
if err != nil {
return nil, err
}
@@ -175,16 +179,17 @@ func TestExtractRates(t *testing.T) {
}
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- tl, err := New(handler, headerLimit, rates, Clock(clock), ExtractRates(RateExtractorFunc(extractRates)))
+ tl, err := New(handler, headerLimit, rates, ExtractRates(RateExtractorFunc(extractRates)))
require.NoError(t, err)
srv := httptest.NewServer(tl)
@@ -203,7 +208,7 @@ func TestExtractRates(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 429, re.StatusCode)
- clock.Sleep(time.Second)
+ clock.Advance(clock.Second)
re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a"))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
@@ -217,16 +222,17 @@ func TestBadRateExtractor(t *testing.T) {
}
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, headerLimit, rates, Clock(clock), ExtractRates(RateExtractorFunc(extractor)))
+ l, err := New(handler, headerLimit, rates, ExtractRates(RateExtractorFunc(extractor)))
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -241,7 +247,7 @@ func TestBadRateExtractor(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 429, re.StatusCode)
- clock.Sleep(time.Second)
+ clock.Advance(clock.Second)
re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a"))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
@@ -255,16 +261,17 @@ func TestExtractorEmpty(t *testing.T) {
}
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, headerLimit, rates, Clock(clock), ExtractRates(RateExtractorFunc(extractor)))
+ l, err := New(handler, headerLimit, rates, ExtractRates(RateExtractorFunc(extractor)))
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -279,7 +286,7 @@ func TestExtractorEmpty(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 429, re.StatusCode)
- clock.Sleep(time.Second)
+ clock.Advance(clock.Second)
re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a"))
require.NoError(t, err)
@@ -289,7 +296,7 @@ func TestExtractorEmpty(t *testing.T) {
func TestInvalidParams(t *testing.T) {
// Rates are missing
rs := NewRateSet()
- err := rs.Add(time.Second, 1, 1)
+ err := rs.Add(clock.Second, 1, 1)
require.NoError(t, err)
// Empty
@@ -305,24 +312,25 @@ func TestInvalidParams(t *testing.T) {
require.Error(t, err)
}
-// We've hit the limit and were able to proceed on the next time run
+// We've hit the limit and were able to proceed on the next time run.
func TestOptions(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
rates := NewRateSet()
- err := rates.Add(time.Second, 1, 1)
+ err := rates.Add(clock.Second, 1, 1)
require.NoError(t, err)
errHandler := utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
- w.Write([]byte(http.StatusText(http.StatusTeapot)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- l, err := New(handler, headerLimit, rates, ErrorHandler(errHandler), Clock(clock))
+ l, err := New(handler, headerLimit, rates, ErrorHandler(errHandler))
require.NoError(t, err)
srv := httptest.NewServer(l)
@@ -346,4 +354,5 @@ func faultyExtractor(_ *http.Request) (string, int64, error) {
}
var headerLimit = utils.ExtractorFunc(headerLimiter)
+
var faultyExtract = utils.ExtractorFunc(faultyExtractor)
diff --git a/roundrobin/RequestRewriteListener.go b/roundrobin/RequestRewriteListener.go
index 02ae454..8de0631 100644
--- a/roundrobin/RequestRewriteListener.go
+++ b/roundrobin/RequestRewriteListener.go
@@ -2,5 +2,5 @@ package roundrobin
import "net/http"
-// RequestRewriteListener function to rewrite request
+// RequestRewriteListener function to rewrite request.
type RequestRewriteListener func(oldReq *http.Request, newReq *http.Request)
diff --git a/roundrobin/rebalancer.go b/roundrobin/rebalancer.go
index 1d182d8..1215573 100644
--- a/roundrobin/rebalancer.go
+++ b/roundrobin/rebalancer.go
@@ -7,23 +7,23 @@ import (
"sync"
"time"
- "github.com/mailgun/timetools"
log "github.com/sirupsen/logrus"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/memmetrics"
"github.com/vulcand/oxy/utils"
)
-// RebalancerOption - functional option setter for rebalancer
+// RebalancerOption - functional option setter for rebalancer.
type RebalancerOption func(*Rebalancer) error
-// Meter measures server performance and returns it's relative value via rating
+// Meter measures server performance and returns it's relative value via rating.
type Meter interface {
Rating() float64
Record(int, time.Duration)
IsReady() bool
}
-// NewMeterFn type of functions to create new Meter
+// NewMeterFn type of functions to create new Meter.
type NewMeterFn func() (Meter, error)
// Rebalancer increases weights on servers that perform better than others. It also rolls back to original weights
@@ -31,12 +31,10 @@ type NewMeterFn func() (Meter, error)
type Rebalancer struct {
// mutex
mtx *sync.Mutex
- // As usual, control time in tests
- clock timetools.TimeProvider
// Time that freezes state machine to accumulate stats after updating the weights
backoffDuration time.Duration
// Timer is set to give probing some time to take place
- timer time.Time
+ timer clock.Time
// server records that remember original weights
servers []*rbServer
// next is internal load balancer next in chain
@@ -57,15 +55,7 @@ type Rebalancer struct {
log *log.Logger
}
-// RebalancerClock sets a clock
-func RebalancerClock(clock timetools.TimeProvider) RebalancerOption {
- return func(r *Rebalancer) error {
- r.clock = clock
- return nil
- }
-}
-
-// RebalancerBackoff sets a beck off duration
+// RebalancerBackoff sets a beck off duration.
func RebalancerBackoff(d time.Duration) RebalancerOption {
return func(r *Rebalancer) error {
r.backoffDuration = d
@@ -73,7 +63,7 @@ func RebalancerBackoff(d time.Duration) RebalancerOption {
}
}
-// RebalancerMeter sets a Meter builder function
+// RebalancerMeter sets a Meter builder function.
func RebalancerMeter(newMeter NewMeterFn) RebalancerOption {
return func(r *Rebalancer) error {
r.newMeter = newMeter
@@ -81,7 +71,7 @@ func RebalancerMeter(newMeter NewMeterFn) RebalancerOption {
}
}
-// RebalancerErrorHandler is a functional argument that sets error handler of the server
+// RebalancerErrorHandler is a functional argument that sets error handler of the server.
func RebalancerErrorHandler(h utils.ErrorHandler) RebalancerOption {
return func(r *Rebalancer) error {
r.errHandler = h
@@ -89,7 +79,7 @@ func RebalancerErrorHandler(h utils.ErrorHandler) RebalancerOption {
}
}
-// RebalancerStickySession sets a sticky session
+// RebalancerStickySession sets a sticky session.
func RebalancerStickySession(stickySession *StickySession) RebalancerOption {
return func(r *Rebalancer) error {
r.stickySession = stickySession
@@ -97,7 +87,7 @@ func RebalancerStickySession(stickySession *StickySession) RebalancerOption {
}
}
-// RebalancerRequestRewriteListener is a functional argument that sets error handler of the server
+// RebalancerRequestRewriteListener is a functional argument that sets error handler of the server.
func RebalancerRequestRewriteListener(rrl RequestRewriteListener) RebalancerOption {
return func(r *Rebalancer) error {
r.requestRewriteListener = rrl
@@ -105,7 +95,7 @@ func RebalancerRequestRewriteListener(rrl RequestRewriteListener) RebalancerOpti
}
}
-// NewRebalancer creates a new Rebalancer
+// NewRebalancer creates a new Rebalancer.
func NewRebalancer(handler balancerHandler, opts ...RebalancerOption) (*Rebalancer, error) {
rb := &Rebalancer{
mtx: &sync.Mutex{},
@@ -119,15 +109,12 @@ func NewRebalancer(handler balancerHandler, opts ...RebalancerOption) (*Rebalanc
return nil, err
}
}
- if rb.clock == nil {
- rb.clock = &timetools.RealTime{}
- }
if rb.backoffDuration == 0 {
- rb.backoffDuration = 10 * time.Second
+ rb.backoffDuration = 10 * clock.Second
}
if rb.newMeter == nil {
rb.newMeter = func() (Meter, error) {
- rc, err := memmetrics.NewRatioCounter(10, time.Second, memmetrics.RatioClock(rb.clock))
+ rc, err := memmetrics.NewRatioCounter(10, clock.Second)
if err != nil {
return nil, err
}
@@ -154,7 +141,7 @@ func RebalancerLogger(l *log.Logger) RebalancerOption {
}
}
-// Servers gets all servers
+// Servers gets all servers.
func (rb *Rebalancer) Servers() []*url.URL {
rb.mtx.Lock()
defer rb.mtx.Unlock()
@@ -164,27 +151,26 @@ func (rb *Rebalancer) Servers() []*url.URL {
func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if rb.log.Level >= log.DebugLevel {
- logEntry := rb.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := rb.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/roundrobin/rebalancer: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/roundrobin/rebalancer: completed ServeHttp on request")
}
pw := utils.NewProxyWriter(w)
- start := rb.clock.UtcNow()
+ start := clock.Now().UTC()
// make shallow copy of request before changing anything to avoid side effects
newReq := *req
stuck := false
if rb.stickySession != nil {
- cookieUrl, present, err := rb.stickySession.GetBackend(&newReq, rb.Servers())
-
+ cookieURL, present, err := rb.stickySession.GetBackend(&newReq, rb.Servers())
if err != nil {
log.Warnf("vulcand/oxy/roundrobin/rebalancer: error using server from cookie: %v", err)
}
if present {
- newReq.URL = cookieUrl
+ newReq.URL = cookieURL
stuck = true
}
}
@@ -198,11 +184,11 @@ func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if log.GetLevel() >= log.DebugLevel {
// log which backend URL we're sending this request to
- log.WithFields(log.Fields{"Request": utils.DumpHttpRequest(req), "ForwardURL": fwdURL}).Debugf("vulcand/oxy/roundrobin/rebalancer: Forwarding this request to URL")
+ log.WithFields(log.Fields{"Request": utils.DumpHTTPRequest(req), "ForwardURL": fwdURL}).Debugf("vulcand/oxy/roundrobin/rebalancer: Forwarding this request to URL")
}
if rb.stickySession != nil {
- rb.stickySession.StickBackend(fwdURL, &w)
+ rb.stickySession.StickBackend(fwdURL, w)
}
newReq.URL = fwdURL
@@ -215,7 +201,7 @@ func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rb.next.Next().ServeHTTP(pw, &newReq)
- rb.recordMetrics(newReq.URL, pw.StatusCode(), rb.clock.UtcNow().Sub(start))
+ rb.recordMetrics(newReq.URL, pw.StatusCode(), clock.Now().UTC().Sub(start))
rb.adjustWeights()
}
@@ -230,9 +216,9 @@ func (rb *Rebalancer) recordMetrics(u *url.URL, code int, latency time.Duration)
func (rb *Rebalancer) reset() {
for _, s := range rb.servers {
s.curWeight = s.origWeight
- rb.next.UpsertServer(s.url, Weight(s.origWeight))
+ _ = rb.next.UpsertServer(s.url, Weight(s.origWeight))
}
- rb.timer = rb.clock.UtcNow().Add(-1 * time.Second)
+ rb.timer = clock.Now().UTC().Add(-1 * clock.Second)
rb.ratings = make([]float64, len(rb.servers))
}
@@ -245,7 +231,7 @@ func (rb *Rebalancer) Wrap(next balancerHandler) error {
return nil
}
-// UpsertServer upsert a server
+// UpsertServer upsert a server.
func (rb *Rebalancer) UpsertServer(u *url.URL, options ...ServerOption) error {
rb.mtx.Lock()
defer rb.mtx.Unlock()
@@ -255,14 +241,14 @@ func (rb *Rebalancer) UpsertServer(u *url.URL, options ...ServerOption) error {
}
weight, _ := rb.next.ServerWeight(u)
if err := rb.upsertServer(u, weight); err != nil {
- rb.next.RemoveServer(u)
+ _ = rb.next.RemoveServer(u)
return err
}
rb.reset()
return nil
}
-// RemoveServer remove a server
+// RemoveServer remove a server.
func (rb *Rebalancer) RemoveServer(u *url.URL) error {
rb.mtx.Lock()
defer rb.mtx.Unlock()
@@ -344,7 +330,7 @@ func (rb *Rebalancer) adjustWeights() {
func (rb *Rebalancer) applyWeights() {
for _, srv := range rb.servers {
rb.log.Debugf("upsert server %v, weight %v", srv.url, srv.curWeight)
- rb.next.UpsertServer(srv.url, Weight(srv.curWeight))
+ _ = rb.next.UpsertServer(srv.url, Weight(srv.curWeight))
}
}
@@ -370,11 +356,11 @@ func (rb *Rebalancer) setMarkedWeights() bool {
}
func (rb *Rebalancer) setTimer() {
- rb.timer = rb.clock.UtcNow().Add(rb.backoffDuration)
+ rb.timer = clock.Now().UTC().Add(rb.backoffDuration)
}
func (rb *Rebalancer) timerExpired() bool {
- return rb.timer.Before(rb.clock.UtcNow())
+ return rb.timer.Before(clock.Now().UTC())
}
func (rb *Rebalancer) metricsReady() bool {
@@ -445,7 +431,7 @@ func (rb *Rebalancer) normalizeWeights() {
return
}
for _, s := range rb.servers {
- s.curWeight = s.curWeight / gcd
+ s.curWeight /= gcd
}
}
@@ -461,7 +447,7 @@ func decrease(target, current int) int {
return adjusted
}
-// rebalancer server record that keeps track of the original weight supplied by user
+// rebalancer server record that keeps track of the original weight supplied by user.
type rbServer struct {
url *url.URL
origWeight int // original weight supplied by user
@@ -471,9 +457,9 @@ type rbServer struct {
}
const (
- // FSMMaxWeight is the maximum weight that handler will set for the server
+ // FSMMaxWeight is the maximum weight that handler will set for the server.
FSMMaxWeight = 4096
- // FSMGrowFactor Multiplier for the server weight
+ // FSMGrowFactor Multiplier for the server weight.
FSMGrowFactor = 4
)
@@ -483,12 +469,12 @@ type codeMeter struct {
codeE int
}
-// Rating gets ratio
+// Rating gets ratio.
func (n *codeMeter) Rating() float64 {
return n.r.Ratio()
}
-// Record records a meter
+// Record records a meter.
func (n *codeMeter) Record(code int, d time.Duration) {
if code >= n.codeS && code < n.codeE {
n.r.IncA(1)
@@ -497,10 +483,10 @@ func (n *codeMeter) Record(code int, d time.Duration) {
}
}
-// IsReady returns true if the counter is ready
+// IsReady returns true if the counter is ready.
func (n *codeMeter) IsReady() bool {
return n.r.IsReady()
}
-// splitThreshold tells how far the value should go from the median + median absolute deviation before it is considered an outlier
+// splitThreshold tells how far the value should go from the median + median absolute deviation before it is considered an outlier.
const splitThreshold = 1.5
diff --git a/roundrobin/rebalancer_test.go b/roundrobin/rebalancer_test.go
index 475bcff..5a43f9b 100644
--- a/roundrobin/rebalancer_test.go
+++ b/roundrobin/rebalancer_test.go
@@ -1,7 +1,7 @@
package roundrobin
import (
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"testing"
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/forward"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
@@ -83,7 +84,7 @@ func TestRebalancerRemoveServer(t *testing.T) {
assert.Equal(t, []string{"b", "b", "b"}, seq(t, proxy.URL, 3))
}
-// Test scenario when one server goes down after what it recovers
+// Test scenario when one server goes down after what it recovers.
func TestRebalancerRecovery(t *testing.T) {
a, b := testutils.NewResponder("a"), testutils.NewResponder("b")
defer a.Close()
@@ -99,9 +100,10 @@ func TestRebalancerRecovery(t *testing.T) {
return &testMeter{}, nil
}
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock))
+ rb, err := NewRebalancer(lb, RebalancerMeter(newMeter))
require.NoError(t, err)
err = rb.UpsertServer(testutils.ParseURI(a.URL))
@@ -119,7 +121,7 @@ func TestRebalancerRecovery(t *testing.T) {
require.NoError(t, err)
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
assert.Equal(t, 1, rb.servers[0].curWeight)
@@ -136,7 +138,7 @@ func TestRebalancerRecovery(t *testing.T) {
require.NoError(t, err)
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
assert.Equal(t, 1, rb.servers[0].curWeight)
@@ -147,7 +149,7 @@ func TestRebalancerRecovery(t *testing.T) {
assert.Equal(t, 1, lb.servers[1].weight)
}
-// Test scenario when increaing the weight on good endpoints made it worse
+// Test scenario when increaing the weight on good endpoints made it worse.
func TestRebalancerCascading(t *testing.T) {
a, b, d := testutils.NewResponder("a"), testutils.NewResponder("b"), testutils.NewResponder("d")
defer a.Close()
@@ -164,9 +166,10 @@ func TestRebalancerCascading(t *testing.T) {
return &testMeter{}, nil
}
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock))
+ rb, err := NewRebalancer(lb, RebalancerMeter(newMeter))
require.NoError(t, err)
err = rb.UpsertServer(testutils.ParseURI(a.URL))
@@ -186,7 +189,7 @@ func TestRebalancerCascading(t *testing.T) {
require.NoError(t, err)
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
// We have increased the load, and the situation became worse as the other servers started failing
@@ -204,7 +207,7 @@ func TestRebalancerCascading(t *testing.T) {
require.NoError(t, err)
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
// the algo reverted it back
@@ -213,7 +216,7 @@ func TestRebalancerCascading(t *testing.T) {
assert.Equal(t, 1, rb.servers[2].curWeight)
}
-// Test scenario when all servers started failing
+// Test scenario when all servers started failing.
func TestRebalancerAllBad(t *testing.T) {
a, b, d := testutils.NewResponder("a"), testutils.NewResponder("b"), testutils.NewResponder("d")
defer a.Close()
@@ -230,9 +233,10 @@ func TestRebalancerAllBad(t *testing.T) {
return &testMeter{}, nil
}
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock))
+ rb, err := NewRebalancer(lb, RebalancerMeter(newMeter))
require.NoError(t, err)
err = rb.UpsertServer(testutils.ParseURI(a.URL))
@@ -254,7 +258,7 @@ func TestRebalancerAllBad(t *testing.T) {
require.NoError(t, err)
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
// load balancer does nothing
@@ -263,7 +267,7 @@ func TestRebalancerAllBad(t *testing.T) {
assert.Equal(t, 1, rb.servers[2].curWeight)
}
-// Removing the server resets the state
+// Removing the server resets the state.
func TestRebalancerReset(t *testing.T) {
a, b, d := testutils.NewResponder("a"), testutils.NewResponder("b"), testutils.NewResponder("d")
defer a.Close()
@@ -280,9 +284,10 @@ func TestRebalancerReset(t *testing.T) {
return &testMeter{}, nil
}
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock))
+ rb, err := NewRebalancer(lb, RebalancerMeter(newMeter))
require.NoError(t, err)
err = rb.UpsertServer(testutils.ParseURI(a.URL))
@@ -304,7 +309,7 @@ func TestRebalancerReset(t *testing.T) {
require.NoError(t, err)
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
// load balancer changed weights
@@ -331,9 +336,10 @@ func TestRebalancerRequestRewriteListenerLive(t *testing.T) {
lb, err := New(fwd)
require.NoError(t, err)
- clock := testutils.GetClock()
+ done := testutils.FreezeTime()
+ defer done()
- rb, err := NewRebalancer(lb, RebalancerBackoff(time.Millisecond), RebalancerClock(clock))
+ rb, err := NewRebalancer(lb, RebalancerBackoff(clock.Millisecond))
require.NoError(t, err)
err = rb.UpsertServer(testutils.ParseURI(a.URL))
@@ -350,7 +356,7 @@ func TestRebalancerRequestRewriteListenerLive(t *testing.T) {
_, _, err = testutils.Get(proxy.URL)
require.NoError(t, err)
if i%10 == 0 {
- clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second)
+ clock.Advance(rb.backoffDuration + clock.Second)
}
}
@@ -416,7 +422,7 @@ func TestRebalancerStickySession(t *testing.T) {
require.NoError(t, err)
defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "a", string(body))
diff --git a/roundrobin/rr.go b/roundrobin/rr.go
index 631a97a..117d3d8 100644
--- a/roundrobin/rr.go
+++ b/roundrobin/rr.go
@@ -11,7 +11,7 @@ import (
"github.com/vulcand/oxy/utils"
)
-// Weight is an optional functional argument that sets weight of the server
+// Weight is an optional functional argument that sets weight of the server.
func Weight(w int) ServerOption {
return func(s *server) error {
if w < 0 {
@@ -22,7 +22,7 @@ func Weight(w int) ServerOption {
}
}
-// ErrorHandler is a functional argument that sets error handler of the server
+// ErrorHandler is a functional argument that sets error handler of the server.
func ErrorHandler(h utils.ErrorHandler) LBOption {
return func(s *RoundRobin) error {
s.errHandler = h
@@ -30,7 +30,7 @@ func ErrorHandler(h utils.ErrorHandler) LBOption {
}
}
-// EnableStickySession enable sticky session
+// EnableStickySession enable sticky session.
func EnableStickySession(stickySession *StickySession) LBOption {
return func(s *RoundRobin) error {
s.stickySession = stickySession
@@ -38,7 +38,7 @@ func EnableStickySession(stickySession *StickySession) LBOption {
}
}
-// RoundRobinRequestRewriteListener is a functional argument that sets error handler of the server
+// RoundRobinRequestRewriteListener is a functional argument that sets error handler of the server.
func RoundRobinRequestRewriteListener(rrl RequestRewriteListener) LBOption {
return func(s *RoundRobin) error {
s.requestRewriteListener = rrl
@@ -46,7 +46,25 @@ func RoundRobinRequestRewriteListener(rrl RequestRewriteListener) LBOption {
}
}
-// RoundRobin implements dynamic weighted round robin load balancer http handler
+// RoundRobinLogger defines the logger the round robin load balancer will use.
+//
+// It defaults to logrus.StandardLogger(), the global logger used by logrus.
+// Deprecated: use Logger instead.
+func RoundRobinLogger(l *log.Logger) LBOption {
+ return Logger(l)
+}
+
+// Logger defines the logger the round robin load balancer will use.
+//
+// It defaults to logrus.StandardLogger(), the global logger used by logrus.
+func Logger(l *log.Logger) LBOption {
+ return func(r *RoundRobin) error {
+ r.log = l
+ return nil
+ }
+}
+
+// RoundRobin implements dynamic weighted round robin load balancer http handler.
type RoundRobin struct {
mutex *sync.Mutex
next http.Handler
@@ -61,7 +79,7 @@ type RoundRobin struct {
log *log.Logger
}
-// New created a new RoundRobin
+// New created a new RoundRobin.
func New(next http.Handler, opts ...LBOption) (*RoundRobin, error) {
rr := &RoundRobin{
next: next,
@@ -83,24 +101,14 @@ func New(next http.Handler, opts ...LBOption) (*RoundRobin, error) {
return rr, nil
}
-// RoundRobinLogger defines the logger the round robin load balancer will use.
-//
-// It defaults to logrus.StandardLogger(), the global logger used by logrus.
-func RoundRobinLogger(l *log.Logger) LBOption {
- return func(r *RoundRobin) error {
- r.log = l
- return nil
- }
-}
-
-// Next returns the next handler
+// Next returns the next handler.
func (r *RoundRobin) Next() http.Handler {
return r.next
}
func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.log.Level >= log.DebugLevel {
- logEntry := r.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := r.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/roundrobin/rr: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/roundrobin/rr: completed ServeHttp on request")
}
@@ -110,7 +118,6 @@ func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) {
stuck := false
if r.stickySession != nil {
cookieURL, present, err := r.stickySession.GetBackend(&newReq, r.Servers())
-
if err != nil {
log.Warnf("vulcand/oxy/roundrobin/rr: error using server from cookie: %v", err)
}
@@ -122,21 +129,21 @@ func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
if !stuck {
- url, err := r.NextServer()
+ uri, err := r.NextServer()
if err != nil {
r.errHandler.ServeHTTP(w, req, err)
return
}
if r.stickySession != nil {
- r.stickySession.StickBackend(url, &w)
+ r.stickySession.StickBackend(uri, w)
}
- newReq.URL = url
+ newReq.URL = uri
}
if r.log.Level >= log.DebugLevel {
// log which backend URL we're sending this request to
- r.log.WithFields(log.Fields{"Request": utils.DumpHttpRequest(req), "ForwardURL": newReq.URL}).Debugf("vulcand/oxy/roundrobin/rr: Forwarding this request to URL")
+ r.log.WithFields(log.Fields{"Request": utils.DumpHTTPRequest(req), "ForwardURL": newReq.URL}).Debugf("vulcand/oxy/roundrobin/rr: Forwarding this request to URL")
}
// Emit event to a listener if one exists
@@ -147,7 +154,7 @@ func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.next.ServeHTTP(w, &newReq)
}
-// NextServer gets the next server
+// NextServer gets the next server.
func (r *RoundRobin) NextServer() (*url.URL, error) {
srv, err := r.nextServer()
if err != nil {
@@ -176,7 +183,7 @@ func (r *RoundRobin) nextServer() (*server, error) {
for {
r.index = (r.index + 1) % len(r.servers)
if r.index == 0 {
- r.currentWeight = r.currentWeight - gcd
+ r.currentWeight -= gcd
if r.currentWeight <= 0 {
r.currentWeight = max
if r.currentWeight == 0 {
@@ -191,7 +198,7 @@ func (r *RoundRobin) nextServer() (*server, error) {
}
}
-// RemoveServer remove a server
+// RemoveServer remove a server.
func (r *RoundRobin) RemoveServer(u *url.URL) error {
r.mutex.Lock()
defer r.mutex.Unlock()
@@ -205,7 +212,7 @@ func (r *RoundRobin) RemoveServer(u *url.URL) error {
return nil
}
-// Servers gets servers URL
+// Servers gets servers URL.
func (r *RoundRobin) Servers() []*url.URL {
r.mutex.Lock()
defer r.mutex.Unlock()
@@ -217,7 +224,7 @@ func (r *RoundRobin) Servers() []*url.URL {
return out
}
-// ServerWeight gets the server weight
+// ServerWeight gets the server weight.
func (r *RoundRobin) ServerWeight(u *url.URL) (int, bool) {
r.mutex.Lock()
defer r.mutex.Unlock()
@@ -228,7 +235,7 @@ func (r *RoundRobin) ServerWeight(u *url.URL) (int, bool) {
return -1, false
}
-// UpsertServer In case if server is already present in the load balancer, returns error
+// UpsertServer In case if server is already present in the load balancer, returns error.
func (r *RoundRobin) UpsertServer(u *url.URL, options ...ServerOption) error {
r.mutex.Lock()
defer r.mutex.Unlock()
@@ -313,13 +320,13 @@ func gcd(a, b int) int {
return a
}
-// ServerOption provides various options for server, e.g. weight
+// ServerOption provides various options for server, e.g. weight.
type ServerOption func(*server) error
-// LBOption provides options for load balancer
+// LBOption provides options for load balancer.
type LBOption func(*RoundRobin) error
-// Set additional parameters for the server can be supplied when adding server
+// Set additional parameters for the server can be supplied when adding server.
type server struct {
url *url.URL
// Relative weight for the enpoint to other enpoints in the load balancer
@@ -328,7 +335,7 @@ type server struct {
var defaultWeight = 1
-// SetDefaultWeight sets the default server weight
+// SetDefaultWeight sets the default server weight.
func SetDefaultWeight(weight int) error {
if weight < 0 {
return fmt.Errorf("default weight should be >= 0")
diff --git a/roundrobin/rr_test.go b/roundrobin/rr_test.go
index 2f2841c..9cea7c8 100644
--- a/roundrobin/rr_test.go
+++ b/roundrobin/rr_test.go
@@ -37,7 +37,7 @@ func TestRemoveBadServer(t *testing.T) {
func TestCustomErrHandler(t *testing.T) {
errHandler := utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
- w.Write([]byte(http.StatusText(http.StatusTeapot)))
+ _, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})
fwd, err := forward.New()
@@ -168,7 +168,7 @@ func TestUpsertWeight(t *testing.T) {
func TestWeighted(t *testing.T) {
require.NoError(t, SetDefaultWeight(0))
- defer SetDefaultWeight(1)
+ defer func() { _ = SetDefaultWeight(1) }()
a := testutils.NewResponder("a")
defer a.Close()
@@ -229,6 +229,8 @@ func TestRequestRewriteListener(t *testing.T) {
}
func seq(t *testing.T, url string, repeat int) []string {
+ t.Helper()
+
var out []string
for i := 0; i < repeat; i++ {
_, body, err := testutils.Get(url)
diff --git a/roundrobin/stickycookie/aes_value.go b/roundrobin/stickycookie/aes_value.go
index a0b411a..3ac7113 100644
--- a/roundrobin/stickycookie/aes_value.go
+++ b/roundrobin/stickycookie/aes_value.go
@@ -13,6 +13,8 @@ import (
"strconv"
"strings"
"time"
+
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
// AESValue manages hashed sticky value.
@@ -41,21 +43,21 @@ func NewAESValue(key []byte, ttl time.Duration) (*AESValue, error) {
func (v *AESValue) Get(raw *url.URL) string {
base := raw.String()
if v.ttl > 0 {
- base = fmt.Sprintf("%s|%d", base, time.Now().UTC().Add(v.ttl).Unix())
+ base = fmt.Sprintf("%s|%d", base, clock.Now().UTC().Add(v.ttl).Unix())
}
// Nonce is the 64bit nanosecond-resolution time, plus 32bits of crypto/rand, for 96bits (12Bytes).
// Theoretically, if 2^32 calls were made in 1 nanoseconds, there might be a repeat.
// Adds ~765ns, and 4B heap in 1 alloc
nonce := make([]byte, 12)
- binary.PutVarint(nonce, time.Now().UnixNano())
+ binary.PutVarint(nonce, clock.Now().UnixNano())
rpend := make([]byte, 4)
if _, err := io.ReadFull(rand.Reader, rpend); err != nil {
// This is a near-impossible error condition on Linux systems.
// An error here means rand.Reader (and thus getrandom(2), and thus /dev/urandom) returned
// less than 4 bytes of data. /dev/urandom is guaranteed to always return the number of
- // bytes requested up to 512 bytes on modern kernels. Behaviour on non-Linux systems
+ // bytes requested up to 512 bytes on modern kernels. Behavior on non-Linux systems
// varies, of course.
panic(err)
}
@@ -109,7 +111,7 @@ func (v *AESValue) fromValue(obfuscatedStr string) (string, error) {
nonce := obfuscated[n:]
obfuscated = obfuscated[:n]
- raw, err := v.block.Open(nil, nonce, []byte(obfuscated), nil)
+ raw, err := v.block.Open(nil, nonce, obfuscated, nil)
if err != nil {
return "", err
}
@@ -126,8 +128,8 @@ func (v *AESValue) fromValue(obfuscatedStr string) (string, error) {
return "", err
}
- if time.Now().UTC().After(time.Unix(i, 0).UTC()) {
- strTime := time.Unix(i, 0).UTC().String()
+ if clock.Now().UTC().After(clock.Unix(i, 0).UTC()) {
+ strTime := clock.Unix(i, 0).UTC().String()
return "", fmt.Errorf("TTL expired: '%s' (%s)\n", raw, strTime)
}
diff --git a/roundrobin/stickycookie/fallback_value.go b/roundrobin/stickycookie/fallback_value.go
index d34d9fe..700f9d1 100644
--- a/roundrobin/stickycookie/fallback_value.go
+++ b/roundrobin/stickycookie/fallback_value.go
@@ -11,7 +11,7 @@ type FallbackValue struct {
to CookieValue
}
-// NewFallbackValue creates a new FallbackValue
+// NewFallbackValue creates a new FallbackValue.
func NewFallbackValue(from CookieValue, to CookieValue) (*FallbackValue, error) {
if from == nil || to == nil {
return nil, errors.New("from and to are mandatory")
diff --git a/roundrobin/stickycookie/fallback_value_test.go b/roundrobin/stickycookie/fallback_value_test.go
index a8146ee..cfda4df 100644
--- a/roundrobin/stickycookie/fallback_value_test.go
+++ b/roundrobin/stickycookie/fallback_value_test.go
@@ -5,11 +5,10 @@ import (
"net/url"
"path"
"testing"
- "time"
-
- "github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
)
func TestFallbackValue_FindURL(t *testing.T) {
@@ -20,7 +19,7 @@ func TestFallbackValue_FindURL(t *testing.T) {
{Scheme: "http", Host: "10.10.10.10", Path: "/"},
}
- aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second)
+ aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second)
require.NoError(t, err)
values := []struct {
@@ -82,7 +81,7 @@ func TestFallbackValue_FindURL_error(t *testing.T) {
hashValue := &HashValue{Salt: "foo"}
rawValue := &RawValue{}
- aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second)
+ aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second)
require.NoError(t, err)
tests := []struct {
diff --git a/roundrobin/stickysessions.go b/roundrobin/stickysessions.go
index 1c882db..c8dbc9f 100644
--- a/roundrobin/stickysessions.go
+++ b/roundrobin/stickysessions.go
@@ -1,6 +1,7 @@
package roundrobin
import (
+ "errors"
"net/http"
"net/url"
"time"
@@ -8,7 +9,7 @@ import (
"github.com/vulcand/oxy/roundrobin/stickycookie"
)
-// CookieOptions has all the options one would like to set on the affinity cookie
+// CookieOptions has all the options one would like to set on the affinity cookie.
type CookieOptions struct {
HTTPOnly bool
Secure bool
@@ -21,20 +22,20 @@ type CookieOptions struct {
SameSite http.SameSite
}
-// StickySession is a mixin for load balancers that implements layer 7 (http cookie) session affinity
+// StickySession is a mixin for load balancers that implements layer 7 (http cookie) session affinity.
type StickySession struct {
cookieName string
cookieValue stickycookie.CookieValue
options CookieOptions
}
-// NewStickySession creates a new StickySession
+// NewStickySession creates a new StickySession.
func NewStickySession(cookieName string) *StickySession {
return &StickySession{cookieName: cookieName, cookieValue: &stickycookie.RawValue{}}
}
// NewStickySessionWithOptions creates a new StickySession whilst allowing for options to
-// shape its affinity cookie such as "httpOnly" or "secure"
+// shape its affinity cookie such as "httpOnly" or "secure".
func NewStickySessionWithOptions(cookieName string, options CookieOptions) *StickySession {
return &StickySession{cookieName: cookieName, options: options, cookieValue: &stickycookie.RawValue{}}
}
@@ -48,11 +49,11 @@ func (s *StickySession) SetCookieValue(value stickycookie.CookieValue) *StickySe
// GetBackend returns the backend URL stored in the sticky cookie, iff the backend is still in the valid list of servers.
func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.URL, bool, error) {
cookie, err := req.Cookie(s.cookieName)
- switch err {
- case nil:
- case http.ErrNoCookie:
- return nil, false, nil
- default:
+ if err != nil {
+ if errors.Is(err, http.ErrNoCookie) {
+ return nil, false, nil
+ }
+
return nil, false, err
}
@@ -61,8 +62,8 @@ func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.
return server, server != nil, err
}
-// StickBackend creates and sets the cookie
-func (s *StickySession) StickBackend(backend *url.URL, w *http.ResponseWriter) {
+// StickBackend creates and sets the cookie.
+func (s *StickySession) StickBackend(backend *url.URL, w http.ResponseWriter) {
opt := s.options
cp := "/"
@@ -81,5 +82,5 @@ func (s *StickySession) StickBackend(backend *url.URL, w *http.ResponseWriter) {
HttpOnly: opt.HTTPOnly,
SameSite: opt.SameSite,
}
- http.SetCookie(*w, cookie)
+ http.SetCookie(w, cookie)
}
diff --git a/roundrobin/stickysessions_test.go b/roundrobin/stickysessions_test.go
index 0a3bc1b..9a3e161 100644
--- a/roundrobin/stickysessions_test.go
+++ b/roundrobin/stickysessions_test.go
@@ -2,16 +2,16 @@ package roundrobin
import (
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
- "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/forward"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/roundrobin/stickycookie"
"github.com/vulcand/oxy/testutils"
)
@@ -51,7 +51,7 @@ func TestBasic(t *testing.T) {
require.NoError(t, err)
defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "a", string(body))
@@ -97,7 +97,7 @@ func TestBasicWithHashValue(t *testing.T) {
resp, err := client.Do(req)
require.NoError(t, err)
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
require.NoError(t, err)
@@ -125,7 +125,7 @@ func TestBasicWithAESValue(t *testing.T) {
sticky := NewStickySession("test")
require.NotNil(t, sticky)
- aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second)
+ aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second)
require.NoError(t, err)
sticky.SetCookieValue(aesValue)
@@ -154,7 +154,7 @@ func TestBasicWithAESValue(t *testing.T) {
resp, err := client.Do(req)
require.NoError(t, err)
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
require.NoError(t, err)
@@ -284,13 +284,13 @@ func TestStickyCookieWithOptions(t *testing.T) {
desc: "Expires",
name: "test",
options: CookieOptions{
- Expires: time.Date(1955, 11, 12, 1, 22, 0, 0, time.UTC),
+ Expires: clock.Date(1955, 11, 12, 1, 22, 0, 0, clock.UTC),
},
expected: &http.Cookie{
Name: "test",
Value: a.URL,
Path: "/",
- Expires: time.Date(1955, 11, 12, 1, 22, 0, 0, time.UTC),
+ Expires: clock.Date(1955, 11, 12, 1, 22, 0, 0, clock.UTC),
RawExpires: "Sat, 12 Nov 1955 01:22:00 GMT",
Raw: fmt.Sprintf("test=%s; Path=/; Expires=Sat, 12 Nov 1955 01:22:00 GMT", a.URL),
},
@@ -327,7 +327,6 @@ func TestStickyCookieWithOptions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
-
fwd, err := forward.New()
require.NoError(t, err)
@@ -390,7 +389,7 @@ func TestRemoveRespondingServer(t *testing.T) {
require.NoError(t, errReq)
defer resp.Body.Close()
- body, errReq := ioutil.ReadAll(resp.Body)
+ body, errReq := io.ReadAll(resp.Body)
require.NoError(t, errReq)
assert.Equal(t, "a", string(body))
@@ -417,7 +416,7 @@ func TestRemoveRespondingServer(t *testing.T) {
require.NoError(t, err)
defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "b", string(body))
@@ -459,7 +458,7 @@ func TestRemoveAllServers(t *testing.T) {
require.NoError(t, errReq)
defer resp.Body.Close()
- body, errReq := ioutil.ReadAll(resp.Body)
+ body, errReq := io.ReadAll(resp.Body)
require.NoError(t, errReq)
assert.Equal(t, "a", string(body))
@@ -508,7 +507,7 @@ func TestBadCookieVal(t *testing.T) {
resp, err := client.Do(req)
require.NoError(t, err)
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "a", string(body))
@@ -519,7 +518,7 @@ func TestBadCookieVal(t *testing.T) {
resp, err = client.Do(req)
require.NoError(t, err)
- _, err = ioutil.ReadAll(resp.Body)
+ _, err = io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
}
@@ -537,10 +536,11 @@ func TestStickySession_GetBackend(t *testing.T) {
rawValue := &stickycookie.RawValue{}
hashValue := &stickycookie.HashValue{}
saltyHashValue := &stickycookie.HashValue{Salt: "test salt"}
- aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second)
+ aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second)
+ require.NoError(t, err)
aesValueInfinite, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 0)
require.NoError(t, err)
- aesValueExpired, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 1*time.Nanosecond)
+ aesValueExpired, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 1*clock.Nanosecond)
require.NoError(t, err)
tests := []struct {
diff --git a/stream/stream.go b/stream/stream.go
index 6f4fdf3..d3dd71d 100644
--- a/stream/stream.go
+++ b/stream/stream.go
@@ -39,26 +39,23 @@ import (
)
const (
- // DefaultMaxBodyBytes No limit by default
+ // DefaultMaxBodyBytes No limit by default.
DefaultMaxBodyBytes = -1
)
// Stream is responsible for buffering requests and responses
-// It buffers large requests and responses to disk,
+// It buffers large requests and responses to disk,.
type Stream struct {
maxRequestBodyBytes int64
maxResponseBodyBytes int64
- retryPredicate hpredicate
-
- next http.Handler
- errHandler utils.ErrorHandler
+ next http.Handler
log *log.Logger
}
-// New returns a new streamer middleware. New() function supports optional functional arguments
+// New returns a new streamer middleware. New() function supports optional functional arguments.
func New(next http.Handler, setters ...optSetter) (*Stream, error) {
strm := &Stream{
next: next,
@@ -97,7 +94,7 @@ func (s *Stream) Wrap(next http.Handler) error {
func (s *Stream) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if s.log.Level >= log.DebugLevel {
- logEntry := s.log.WithField("Request", utils.DumpHttpRequest(req))
+ logEntry := s.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/stream: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/stream: completed ServeHttp on request")
}
diff --git a/stream/stream_test.go b/stream/stream_test.go
index f9026d2..803e8ab 100644
--- a/stream/stream_test.go
+++ b/stream/stream_test.go
@@ -4,17 +4,17 @@ import (
"bufio"
"crypto/tls"
"fmt"
- "io/ioutil"
+ "io"
"net"
"net/http"
"net/http/httptest"
"testing"
- "time"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/forward"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
)
@@ -30,7 +30,7 @@ func (n noOpIoWriter) Write(bytes []byte) (int, error) {
func TestSimple(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -61,7 +61,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
var reqBody string
var contentLength int64
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- body, err := ioutil.ReadAll(req.Body)
+ body, err := io.ReadAll(req.Body)
require.NoError(t, err)
reqBody = string(body)
contentLength = req.ContentLength
@@ -71,13 +71,13 @@ func TestChunkedEncodingSuccess(t *testing.T) {
if !ok {
panic("expected http.ResponseWriter to be an http.Flusher")
}
- fmt.Fprint(w, "Response")
+ _, _ = fmt.Fprint(w, "Response")
flusher.Flush()
- time.Sleep(time.Duration(500) * time.Millisecond)
- fmt.Fprint(w, "in")
+ clock.Sleep(500 * clock.Millisecond)
+ _, _ = fmt.Fprint(w, "in")
flusher.Flush()
- time.Sleep(time.Duration(500) * time.Millisecond)
- fmt.Fprint(w, "Chunks")
+ clock.Sleep(500 * clock.Millisecond)
+ _, _ = fmt.Fprint(w, "Chunks")
flusher.Flush()
})
defer srv.Close()
@@ -101,7 +101,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
conn, err := net.Dial("tcp", testutils.ParseURI(proxy.URL).Host)
require.NoError(t, err)
- fmt.Fprint(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
+ _, _ = fmt.Fprint(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
reader := bufio.NewReader(conn)
status, err := reader.ReadString('\n')
@@ -122,7 +122,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
func TestRequestLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
defer srv.Close()
@@ -150,7 +150,7 @@ func TestRequestLimitReached(t *testing.T) {
func TestResponseLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello, this response is too large"))
+ _, _ = w.Write([]byte("hello, this response is too large"))
})
defer srv.Close()
@@ -178,7 +178,7 @@ func TestResponseLimitReached(t *testing.T) {
func TestFileStreamingResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello, this response is too large to fit in memory"))
+ _, _ = w.Write([]byte("hello, this response is too large to fit in memory"))
})
defer srv.Close()
@@ -207,7 +207,7 @@ func TestFileStreamingResponse(t *testing.T) {
func TestCustomErrorHandler(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello, this response is too large"))
+ _, _ = w.Write([]byte("hello, this response is too large"))
})
defer srv.Close()
@@ -288,11 +288,11 @@ func TestNoBody(t *testing.T) {
assert.Equal(t, http.StatusOK, re.StatusCode)
}
-// Make sure that stream handler preserves TLS settings
+// Make sure that stream handler preserves TLS settings.
func TestPreservesTLS(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
- w.Write([]byte("ok"))
+ _, _ = w.Write([]byte("ok"))
})
defer srv.Close()
diff --git a/stream/threshold.go b/stream/threshold.go
index 0f0a453..a58bfab 100644
--- a/stream/threshold.go
+++ b/stream/threshold.go
@@ -7,7 +7,7 @@ import (
"github.com/vulcand/predicate"
)
-// IsValidExpression check if it's a valid expression
+// IsValidExpression check if it's a valid expression.
func IsValidExpression(expr string) bool {
_, err := parseExpression(expr)
return err == nil
@@ -21,7 +21,7 @@ type context struct {
type hpredicate func(*context) bool
-// Parses expression in the go language into Failover predicates
+// Parses expression in the go language into Failover predicates.
func parseExpression(in string) (hpredicate, error) {
p, err := predicate.NewParser(predicate.Def{
Operators: predicate.Operators{
@@ -56,16 +56,17 @@ func parseExpression(in string) (hpredicate, error) {
}
type toString func(c *context) string
+
type toInt func(c *context) int
-// RequestMethod returns mapper of the request to its method e.g. POST
+// RequestMethod returns mapper of the request to its method e.g. POST.
func requestMethod() toString {
return func(c *context) string {
return c.r.Method
}
}
-// Attempts returns mapper of the request to the number of proxy attempts
+// Attempts returns mapper of the request to the number of proxy attempts.
func attempts() toInt {
return func(c *context) int {
return c.attempt
@@ -86,7 +87,7 @@ func isNetworkError() hpredicate {
}
}
-// and returns predicate by joining the passed predicates with logical 'and'
+// and returns predicate by joining the passed predicates with logical 'and'.
func and(fns ...hpredicate) hpredicate {
return func(c *context) bool {
for _, fn := range fns {
@@ -98,7 +99,7 @@ func and(fns ...hpredicate) hpredicate {
}
}
-// or returns predicate by joining the passed predicates with logical 'or'
+// or returns predicate by joining the passed predicates with logical 'or'.
func or(fns ...hpredicate) hpredicate {
return func(c *context) bool {
for _, fn := range fns {
@@ -110,14 +111,14 @@ func or(fns ...hpredicate) hpredicate {
}
}
-// not creates negation of the passed predicate
+// not creates negation of the passed predicate.
func not(p hpredicate) hpredicate {
return func(c *context) bool {
return !p(c)
}
}
-// eq returns predicate that tests for equality of the value of the mapper and the constant
+// eq returns predicate that tests for equality of the value of the mapper and the constant.
func eq(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toString:
@@ -128,7 +129,7 @@ func eq(m interface{}, value interface{}) (hpredicate, error) {
return nil, fmt.Errorf("unsupported argument: %T", m)
}
-// neq returns predicate that tests for inequality of the value of the mapper and the constant
+// neq returns predicate that tests for inequality of the value of the mapper and the constant.
func neq(m interface{}, value interface{}) (hpredicate, error) {
p, err := eq(m, value)
if err != nil {
@@ -137,16 +138,17 @@ func neq(m interface{}, value interface{}) (hpredicate, error) {
return not(p), nil
}
-// lt returns predicate that tests that value of the mapper function is less than the constant
+// lt returns predicate that tests that value of the mapper function is less than the constant.
func lt(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
return intLT(mapper, value)
+ default:
+ return nil, fmt.Errorf("unsupported argument: %T", m)
}
- return nil, fmt.Errorf("unsupported argument: %T", m)
}
-// le returns predicate that tests that value of the mapper function is less or equal than the constant
+// le returns predicate that tests that value of the mapper function is less or equal than the constant.
func le(m interface{}, value interface{}) (hpredicate, error) {
l, err := lt(m, value)
if err != nil {
@@ -161,16 +163,17 @@ func le(m interface{}, value interface{}) (hpredicate, error) {
}, nil
}
-// gt returns predicate that tests that value of the mapper function is greater than the constant
+// gt returns predicate that tests that value of the mapper function is greater than the constant.
func gt(m interface{}, value interface{}) (hpredicate, error) {
switch mapper := m.(type) {
case toInt:
return intGT(mapper, value)
+ default:
+ return nil, fmt.Errorf("unsupported argument: %T", m)
}
- return nil, fmt.Errorf("unsupported argument: %T", m)
}
-// ge returns predicate that tests that value of the mapper function is less or equal than the constant
+// ge returns predicate that tests that value of the mapper function is less or equal than the constant.
func ge(m interface{}, value interface{}) (hpredicate, error) {
g, err := gt(m, value)
if err != nil {
diff --git a/testutils/utils.go b/testutils/utils.go
index 94a55f7..3d21df8 100644
--- a/testutils/utils.go
+++ b/testutils/utils.go
@@ -3,30 +3,29 @@ package testutils
import (
"crypto/tls"
"errors"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
- "time"
- "github.com/mailgun/timetools"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/utils"
)
-// NewHandler creates a new Server
+// NewHandler creates a new Server.
func NewHandler(handler http.HandlerFunc) *httptest.Server {
return httptest.NewServer(handler)
}
-// NewResponder creates a new Server with response
+// NewResponder creates a new Server with response.
func NewResponder(response string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(response))
+ _, _ = w.Write([]byte(response))
}))
}
-// ParseURI is the version of url.ParseRequestURI that panics if incorrect, helpful to shorten the tests
+// ParseURI is the version of url.ParseRequestURI that panics if incorrect, helpful to shorten the tests.
func ParseURI(uri string) *url.URL {
out, err := url.ParseRequestURI(uri)
if err != nil {
@@ -35,7 +34,7 @@ func ParseURI(uri string) *url.URL {
return out
}
-// ReqOpts request options
+// ReqOpts request options.
type ReqOpts struct {
Host string
Method string
@@ -44,10 +43,10 @@ type ReqOpts struct {
Auth *utils.BasicAuth
}
-// ReqOption request option type
+// ReqOption request option type.
type ReqOption func(o *ReqOpts) error
-// Method sets request method
+// Method sets request method.
func Method(m string) ReqOption {
return func(o *ReqOpts) error {
o.Method = m
@@ -55,7 +54,7 @@ func Method(m string) ReqOption {
}
}
-// Host sets request host
+// Host sets request host.
func Host(h string) ReqOption {
return func(o *ReqOpts) error {
o.Host = h
@@ -63,7 +62,7 @@ func Host(h string) ReqOption {
}
}
-// Body sets request body
+// Body sets request body.
func Body(b string) ReqOption {
return func(o *ReqOpts) error {
o.Body = b
@@ -71,7 +70,7 @@ func Body(b string) ReqOption {
}
}
-// Header sets request header
+// Header sets request header.
func Header(name, val string) ReqOption {
return func(o *ReqOpts) error {
if o.Headers == nil {
@@ -82,7 +81,7 @@ func Header(name, val string) ReqOption {
}
}
-// Headers sets request headers
+// Headers sets request headers.
func Headers(h http.Header) ReqOption {
return func(o *ReqOpts) error {
if o.Headers == nil {
@@ -93,7 +92,7 @@ func Headers(h http.Header) ReqOption {
}
}
-// BasicAuth sets request basic auth
+// BasicAuth sets request basic auth.
func BasicAuth(username, password string) ReqOption {
return func(o *ReqOpts) error {
o.Auth = &utils.BasicAuth{
@@ -104,8 +103,8 @@ func BasicAuth(username, password string) ReqOption {
}
}
-// MakeRequest create and do a request
-func MakeRequest(url string, opts ...ReqOption) (*http.Response, []byte, error) {
+// MakeRequest create and do a request.
+func MakeRequest(uri string, opts ...ReqOption) (*http.Response, []byte, error) {
o := &ReqOpts{}
for _, s := range opts {
if err := s(o); err != nil {
@@ -117,7 +116,7 @@ func MakeRequest(url string, opts ...ReqOption) (*http.Response, []byte, error)
o.Method = http.MethodGet
}
- request, err := http.NewRequest(o.Method, url, strings.NewReader(o.Body))
+ request, err := http.NewRequest(o.Method, uri, strings.NewReader(o.Body))
if err != nil {
return nil, nil, err
}
@@ -130,12 +129,12 @@ func MakeRequest(url string, opts ...ReqOption) (*http.Response, []byte, error)
request.Header.Set("Authorization", o.Auth.String())
}
- if len(o.Host) != 0 {
+ if o.Host != "" {
request.Host = o.Host
}
var tr *http.Transport
- if strings.HasPrefix(url, "https") {
+ if strings.HasPrefix(uri, "https") {
tr = &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
@@ -157,27 +156,27 @@ func MakeRequest(url string, opts ...ReqOption) (*http.Response, []byte, error)
}
response, err := client.Do(request)
if err == nil {
- bodyBytes, errRead := ioutil.ReadAll(response.Body)
+ bodyBytes, errRead := io.ReadAll(response.Body)
return response, bodyBytes, errRead
}
return response, nil, err
}
-// Get do a GET request
-func Get(url string, opts ...ReqOption) (*http.Response, []byte, error) {
+// Get do a GET request.
+func Get(uri string, opts ...ReqOption) (*http.Response, []byte, error) {
opts = append(opts, Method(http.MethodGet))
- return MakeRequest(url, opts...)
+ return MakeRequest(uri, opts...)
}
-// Post do a POST request
-func Post(url string, opts ...ReqOption) (*http.Response, []byte, error) {
+// Post do a POST request.
+func Post(uri string, opts ...ReqOption) (*http.Response, []byte, error) {
opts = append(opts, Method(http.MethodPost))
- return MakeRequest(url, opts...)
+ return MakeRequest(uri, opts...)
}
-// GetClock gets a FreezedTime
-func GetClock() *timetools.FreezedTime {
- return &timetools.FreezedTime{
- CurrentTime: time.Date(2012, 3, 4, 5, 6, 7, 0, time.UTC),
- }
+// FreezeTime to the predetermined time. Returns a function that should be
+// deferred to unfreeze time. Meant for testing.
+func FreezeTime() func() {
+ clock.Freeze(clock.Date(2012, 3, 4, 5, 6, 7, 0, clock.UTC))
+ return clock.Unfreeze
}
diff --git a/trace/trace.go b/trace/trace.go
index 48b6d1d..ce5e51d 100644
--- a/trace/trace.go
+++ b/trace/trace.go
@@ -11,13 +11,14 @@ import (
"time"
log "github.com/sirupsen/logrus"
+ "github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/utils"
)
-// Option is a functional option setter for Tracer
+// Option is a functional option setter for Tracer.
type Option func(*Tracer) error
-// ErrorHandler is a functional argument that sets error handler of the server
+// ErrorHandler is a functional argument that sets error handler of the server.
func ErrorHandler(h utils.ErrorHandler) Option {
return func(t *Tracer) error {
t.errHandler = h
@@ -25,7 +26,7 @@ func ErrorHandler(h utils.ErrorHandler) Option {
}
}
-// RequestHeaders adds request headers to capture
+// RequestHeaders adds request headers to capture.
func RequestHeaders(headers ...string) Option {
return func(t *Tracer) error {
t.reqHeaders = append(t.reqHeaders, headers...)
@@ -33,7 +34,7 @@ func RequestHeaders(headers ...string) Option {
}
}
-// ResponseHeaders adds response headers to capture
+// ResponseHeaders adds response headers to capture.
func ResponseHeaders(headers ...string) Option {
return func(t *Tracer) error {
t.respHeaders = append(t.respHeaders, headers...)
@@ -41,7 +42,7 @@ func ResponseHeaders(headers ...string) Option {
}
}
-// Tracer records request and response emitting JSON structured data to the output
+// Tracer records request and response emitting JSON structured data to the output.
type Tracer struct {
errHandler utils.ErrorHandler
next http.Handler
@@ -84,11 +85,11 @@ func Logger(l *log.Logger) Option {
}
func (t *Tracer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
- start := time.Now()
+ start := clock.Now()
pw := utils.NewProxyWriterWithLogger(w, t.log)
t.next.ServeHTTP(pw, req)
- l := t.newRecord(req, pw, time.Since(start))
+ l := t.newRecord(req, pw, clock.Since(start))
if err := json.NewEncoder(t.writer).Encode(l); err != nil {
t.log.Errorf("Failed to marshal request: %v", err)
}
@@ -106,7 +107,7 @@ func (t *Tracer) newRecord(req *http.Request, pw *utils.ProxyWriter, diff time.D
Response: Response{
Code: pw.StatusCode(),
BodyBytes: bodyBytes(pw.Header()),
- Roundtrip: float64(diff) / float64(time.Millisecond),
+ Roundtrip: float64(diff) / float64(clock.Millisecond),
Headers: captureHeaders(pw.Header(), t.respHeaders),
},
}
@@ -141,13 +142,13 @@ func captureHeaders(in http.Header, headers []string) http.Header {
return out
}
-// Record represents a structured request and response record
+// Record represents a structured request and response record.
type Record struct {
Request Request `json:"request"`
Response Response `json:"response"`
}
-// Request contains information about an HTTP request
+// Request contains information about an HTTP request.
type Request struct {
Method string `json:"method"` // Method - request method
BodyBytes int64 `json:"body_bytes"` // BodyBytes - size of request body in bytes
@@ -156,7 +157,7 @@ type Request struct {
TLS *TLS `json:"tls,omitempty"` // TLS - optional TLS record, will be recorded if it's a TLS connection
}
-// Response contains information about HTTP response
+// Response contains information about HTTP response.
type Response struct {
Code int `json:"code"` // Code - response status code
Roundtrip float64 `json:"roundtrip"` // Roundtrip - round trip time in milliseconds
@@ -164,7 +165,7 @@ type Response struct {
BodyBytes int64 `json:"body_bytes"` // BodyBytes - size of response body in bytes
}
-// TLS contains information about this TLS connection
+// TLS contains information about this TLS connection.
type TLS struct {
Version string `json:"version"` // Version - TLS version
Resume bool `json:"resume"` // Resume tells if the session has been re-used (session tickets)
diff --git a/trace/trace_test.go b/trace/trace_test.go
index 9d9914c..338355d 100644
--- a/trace/trace_test.go
+++ b/trace/trace_test.go
@@ -20,7 +20,7 @@ import (
func TestTraceSimple(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Length", "5")
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
trace := &bytes.Buffer{}
@@ -53,7 +53,7 @@ func TestTraceCaptureHeaders(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
utils.CopyHeaders(w.Header(), respHeaders)
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
trace := &bytes.Buffer{}
@@ -77,7 +77,7 @@ func TestTraceCaptureHeaders(t *testing.T) {
func TestTraceTLS(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- w.Write([]byte("hello"))
+ _, _ = w.Write([]byte("hello"))
})
trace := &bytes.Buffer{}
@@ -98,12 +98,12 @@ func TestTraceTLS(t *testing.T) {
conn, err := tls.Dial("tcp", u.Host, config)
require.NoError(t, err)
- fmt.Fprint(conn, "GET / HTTP/1.0\r\n\r\n")
+ _, _ = fmt.Fprint(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
require.NoError(t, err)
assert.Equal(t, "HTTP/1.0 200 OK\r\n", status)
state := conn.ConnectionState()
- conn.Close()
+ _ = conn.Close()
var r *Record
require.NoError(t, json.Unmarshal(trace.Bytes(), &r))
diff --git a/utils/auth.go b/utils/auth.go
index 4fd819c..4bb2b0f 100644
--- a/utils/auth.go
+++ b/utils/auth.go
@@ -6,7 +6,7 @@ import (
"strings"
)
-// BasicAuth basic auth information
+// BasicAuth basic auth information.
type BasicAuth struct {
Username string
Password string
@@ -17,11 +17,11 @@ func (ba *BasicAuth) String() string {
return fmt.Sprintf("Basic %s", encoded)
}
-// ParseAuthHeader creates a new BasicAuth from header values
+// ParseAuthHeader creates a new BasicAuth from header values.
func ParseAuthHeader(header string) (*BasicAuth, error) {
values := strings.Fields(header)
if len(values) != 2 {
- return nil, fmt.Errorf(fmt.Sprintf("Failed to parse header '%s'", header))
+ return nil, fmt.Errorf("Failed to parse header '%s'", header)
}
authType := strings.ToLower(values[0])
diff --git a/utils/auth_test.go b/utils/auth_test.go
index 8951b90..7ff407b 100644
--- a/utils/auth_test.go
+++ b/utils/auth_test.go
@@ -8,7 +8,7 @@ import (
)
// Just to make sure we don't panic, return err and not
-// username and pass and cover the function
+// username and pass and cover the function.
func TestParseBadHeaders(t *testing.T) {
headers := []string{
// just empty string
@@ -29,7 +29,7 @@ func TestParseBadHeaders(t *testing.T) {
}
// Just to make sure we don't panic, return err and not
-// username and pass and cover the function
+// username and pass and cover the function.
func TestParseSuccess(t *testing.T) {
headers := []struct {
Header string
@@ -50,11 +50,11 @@ func TestParseSuccess(t *testing.T) {
BasicAuth{Username: "Aladdin", Password: ""},
},
}
+
for _, h := range headers {
request, err := ParseAuthHeader(h.Header)
require.NoError(t, err)
assert.Equal(t, h.Expected.Username, request.Username)
assert.Equal(t, h.Expected.Password, request.Password)
-
}
}
diff --git a/utils/dumpreq.go b/utils/dumpreq.go
index eecb222..f1eab2d 100644
--- a/utils/dumpreq.go
+++ b/utils/dumpreq.go
@@ -9,8 +9,12 @@ import (
"net/url"
)
-// SerializableHttpRequest serializable HTTP request
-type SerializableHttpRequest struct {
+// SerializableHttpRequest alias on SerializableHTTPRequest.
+// Deprecated: use SerializableHTTPRequest instead.
+type SerializableHttpRequest = SerializableHTTPRequest
+
+// SerializableHTTPRequest serializable HTTP request.
+type SerializableHTTPRequest struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
@@ -29,13 +33,13 @@ type SerializableHttpRequest struct {
TLS *tls.ConnectionState
}
-// Clone clone a request
-func Clone(r *http.Request) *SerializableHttpRequest {
+// Clone clone a request.
+func Clone(r *http.Request) *SerializableHTTPRequest {
if r == nil {
return nil
}
- rc := new(SerializableHttpRequest)
+ rc := new(SerializableHTTPRequest)
rc.Method = r.Method
rc.URL = r.URL
rc.Proto = r.Proto
@@ -49,16 +53,28 @@ func Clone(r *http.Request) *SerializableHttpRequest {
return rc
}
-// ToJson serializes to JSON
-func (s *SerializableHttpRequest) ToJson() string {
+// ToJson serializes to JSON.
+// Deprecated: use ToJSON instead.
+func (s *SerializableHTTPRequest) ToJson() string {
+ return s.ToJSON()
+}
+
+// ToJSON serializes to JSON.
+func (s *SerializableHTTPRequest) ToJSON() string {
jsonVal, err := json.Marshal(s)
if err != nil || jsonVal == nil {
- return fmt.Sprintf("Error marshalling SerializableHttpRequest to json: %s", err)
+ return fmt.Sprintf("Error marshalling SerializableHTTPRequest to json: %s", err)
}
return string(jsonVal)
}
-// DumpHttpRequest dump a HTTP request to JSON
+// DumpHttpRequest dump a HTTP request to JSON.
+// Deprecated: use DumpHTTPRequest instead.
func DumpHttpRequest(req *http.Request) string {
- return Clone(req).ToJson()
+ return DumpHTTPRequest(req)
+}
+
+// DumpHTTPRequest dump a HTTP request to JSON.
+func DumpHTTPRequest(req *http.Request) string {
+ return Clone(req).ToJSON()
}
diff --git a/utils/dumpreq_test.go b/utils/dumpreq_test.go
index 8c86973..67c246d 100644
--- a/utils/dumpreq_test.go
+++ b/utils/dumpreq_test.go
@@ -19,7 +19,7 @@ func (r *readCloserTestImpl) Close() error {
}
// Just to make sure we don't panic, return err and not
-// username and pass and cover the function
+// username and pass and cover the function.
func TestHttpReqToString(t *testing.T) {
req := &http.Request{
URL: &url.URL{Host: "localhost:2374", Path: "/unittest"},
@@ -28,5 +28,5 @@ func TestHttpReqToString(t *testing.T) {
Body: &readCloserTestImpl{},
}
- assert.True(t, len(DumpHttpRequest(req)) > 0)
+ assert.True(t, len(DumpHTTPRequest(req)) > 0)
}
diff --git a/utils/handler.go b/utils/handler.go
index 24b9e3a..fde2b34 100644
--- a/utils/handler.go
+++ b/utils/handler.go
@@ -9,21 +9,21 @@ import (
log "github.com/sirupsen/logrus"
)
-// StatusClientClosedRequest non-standard HTTP status code for client disconnection
+// StatusClientClosedRequest non-standard HTTP status code for client disconnection.
const StatusClientClosedRequest = 499
-// StatusClientClosedRequestText non-standard HTTP status for client disconnection
+// StatusClientClosedRequestText non-standard HTTP status for client disconnection.
const StatusClientClosedRequestText = "Client Closed Request"
-// ErrorHandler error handler
+// ErrorHandler error handler.
type ErrorHandler interface {
ServeHTTP(w http.ResponseWriter, req *http.Request, err error)
}
-// DefaultHandler default error handler
+// DefaultHandler default error handler.
var DefaultHandler ErrorHandler = &StdHandler{}
-// StdHandler Standard error handler
+// StdHandler Standard error handler.
type StdHandler struct{}
func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) {
@@ -42,7 +42,7 @@ func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err err
}
w.WriteHeader(statusCode)
- w.Write([]byte(statusText(statusCode)))
+ _, _ = w.Write([]byte(statusText(statusCode)))
log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
}
@@ -53,7 +53,7 @@ func statusText(statusCode int) string {
return http.StatusText(statusCode)
}
-// ErrorHandlerFunc error handler function type
+// ErrorHandlerFunc error handler function type.
type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, error)
// ServeHTTP calls f(w, r).
diff --git a/utils/netutils.go b/utils/netutils.go
index 692d300..b6845f7 100644
--- a/utils/netutils.go
+++ b/utils/netutils.go
@@ -12,7 +12,7 @@ import (
log "github.com/sirupsen/logrus"
)
-// ProxyWriter calls recorder, used to debug logs
+// ProxyWriter calls recorder, used to debug logs.
type ProxyWriter struct {
w http.ResponseWriter
code int
@@ -21,12 +21,12 @@ type ProxyWriter struct {
log *log.Logger
}
-// NewProxyWriter creates a new ProxyWriter
+// NewProxyWriter creates a new ProxyWriter.
func NewProxyWriter(w http.ResponseWriter) *ProxyWriter {
return NewProxyWriterWithLogger(w, log.StandardLogger())
}
-// NewProxyWriterWithLogger creates a new ProxyWriter
+// NewProxyWriterWithLogger creates a new ProxyWriter.
func NewProxyWriterWithLogger(w http.ResponseWriter, l *log.Logger) *ProxyWriter {
return &ProxyWriter{
w: w,
@@ -34,7 +34,7 @@ func NewProxyWriterWithLogger(w http.ResponseWriter, l *log.Logger) *ProxyWriter
}
}
-// StatusCode gets status code
+// StatusCode gets status code.
func (p *ProxyWriter) StatusCode() int {
if p.code == 0 {
// per contract standard lib will set this to http.StatusOK if not set
@@ -44,28 +44,28 @@ func (p *ProxyWriter) StatusCode() int {
return p.code
}
-// GetLength gets content length
+// GetLength gets content length.
func (p *ProxyWriter) GetLength() int64 {
return p.length
}
-// Header gets response header
+// Header gets response header.
func (p *ProxyWriter) Header() http.Header {
return p.w.Header()
}
func (p *ProxyWriter) Write(buf []byte) (int, error) {
- p.length = p.length + int64(len(buf))
+ p.length += int64(len(buf))
return p.w.Write(buf)
}
-// WriteHeader writes status code
+// WriteHeader writes status code.
func (p *ProxyWriter) WriteHeader(code int) {
p.code = code
p.w.WriteHeader(code)
}
-// Flush flush the writer
+// Flush flush the writer.
func (p *ProxyWriter) Flush() {
if f, ok := p.w.(http.Flusher); ok {
f.Flush()
@@ -91,7 +91,7 @@ func (p *ProxyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, fmt.Errorf("the response writer that was wrapped in this proxy, does not implement http.Hijacker. It is of type: %v", reflect.TypeOf(p.w))
}
-// NewBufferWriter creates a new BufferWriter
+// NewBufferWriter creates a new BufferWriter.
func NewBufferWriter(w io.WriteCloser) *BufferWriter {
return &BufferWriter{
W: w,
@@ -99,19 +99,19 @@ func NewBufferWriter(w io.WriteCloser) *BufferWriter {
}
}
-// BufferWriter buffer writer
+// BufferWriter buffer writer.
type BufferWriter struct {
H http.Header
Code int
W io.WriteCloser
}
-// Close close the writer
+// Close close the writer.
func (b *BufferWriter) Close() error {
return b.W.Close()
}
-// Header gets response header
+// Header gets response header.
func (b *BufferWriter) Header() http.Header {
return b.H
}
@@ -120,7 +120,7 @@ func (b *BufferWriter) Write(buf []byte) (int, error) {
return b.W.Write(buf)
}
-// WriteHeader writes status code
+// WriteHeader writes status code.
func (b *BufferWriter) WriteHeader(code int) {
b.Code = code
}
@@ -156,24 +156,25 @@ func NopWriteCloser(w io.Writer) io.WriteCloser {
return &nopWriteCloser{Writer: w}
}
-// CopyURL provides update safe copy by avoiding shallow copying User field
+// CopyURL provides update safe copy by avoiding shallow copying User field.
func CopyURL(i *url.URL) *url.URL {
out := *i
if i.User != nil {
- out.User = &(*i.User)
+ u := *i.User
+ out.User = &u
}
return &out
}
// CopyHeaders copies http headers from source to destination, it
-// does not overide, but adds multiple headers
+// does not override, but adds multiple headers.
func CopyHeaders(dst http.Header, src http.Header) {
for k, vv := range src {
dst[k] = append(dst[k], vv...)
}
}
-// HasHeaders determines whether any of the header names is present in the http headers
+// HasHeaders determines whether any of the header names is present in the http headers.
func HasHeaders(names []string, headers http.Header) bool {
for _, h := range names {
if headers.Get(h) != "" {
@@ -183,7 +184,7 @@ func HasHeaders(names []string, headers http.Header) bool {
return false
}
-// RemoveHeaders removes the header with the given names from the headers map
+// RemoveHeaders removes the header with the given names from the headers map.
func RemoveHeaders(headers http.Header, names ...string) {
for _, h := range names {
headers.Del(h)
diff --git a/utils/netutils_test.go b/utils/netutils_test.go
index 2db0040..8d2013f 100644
--- a/utils/netutils_test.go
+++ b/utils/netutils_test.go
@@ -8,9 +8,10 @@ import (
"github.com/stretchr/testify/assert"
)
-// Make sure copy does it right, so the copied url
-// is safe to alter without modifying the other
+// Make sure copy does it right, so the copied url is safe to alter without modifying the other.
func TestCopyUrl(t *testing.T) {
+ userinfo := url.UserPassword("foo", "secret")
+
urlA := &url.URL{
Scheme: "http",
Host: "localhost:5000",
@@ -18,17 +19,23 @@ func TestCopyUrl(t *testing.T) {
Opaque: "opaque",
RawQuery: "a=1&b=2",
Fragment: "#hello",
- User: &url.Userinfo{},
+ User: userinfo,
}
urlB := CopyURL(urlA)
assert.Equal(t, urlA, urlB)
+ *userinfo = *url.User("bar")
+
+ assert.Equal(t, urlA.User, userinfo)
+ assert.NotEqual(t, urlA.User, urlB.User)
+
urlB.Scheme = "https"
+
assert.NotEqual(t, urlA, urlB)
}
-// Make sure copy headers is not shallow and copies all headers
+// Make sure copy headers is not shallow and copies all headers.
func TestCopyHeaders(t *testing.T) {
source, destination := make(http.Header), make(http.Header)
source.Add("a", "b")
diff --git a/utils/source.go b/utils/source.go
index 5306b59..d8d6c27 100644
--- a/utils/source.go
+++ b/utils/source.go
@@ -8,23 +8,23 @@ import (
// SourceExtractor extracts the source from the request, e.g. that may be client ip, or particular header that
// identifies the source. amount stands for amount of connections the source consumes, usually 1 for connection limiters
-// error should be returned when source can not be identified
+// error should be returned when source can not be identified.
type SourceExtractor interface {
Extract(req *http.Request) (token string, amount int64, err error)
}
-// ExtractorFunc extractor function type
+// ExtractorFunc extractor function type.
type ExtractorFunc func(req *http.Request) (token string, amount int64, err error)
-// Extract extract from request
+// Extract extract from request.
func (f ExtractorFunc) Extract(req *http.Request) (string, int64, error) {
return f(req)
}
-// ExtractSource extract source function type
+// ExtractSource extract source function type.
type ExtractSource func(req *http.Request)
-// NewExtractor creates a new SourceExtractor
+// NewExtractor creates a new SourceExtractor.
func NewExtractor(variable string) (SourceExtractor, error) {
if variable == "client.ip" {
return ExtractorFunc(extractClientIP), nil
@@ -34,7 +34,7 @@ func NewExtractor(variable string) (SourceExtractor, error) {
}
if strings.HasPrefix(variable, "request.header.") {
header := strings.TrimPrefix(variable, "request.header.")
- if len(header) == 0 {
+ if header == "" {
return nil, fmt.Errorf("wrong header: %s", header)
}
return makeHeaderExtractor(header), nil
@@ -44,7 +44,7 @@ func NewExtractor(variable string) (SourceExtractor, error) {
func extractClientIP(req *http.Request) (string, int64, error) {
vals := strings.SplitN(req.RemoteAddr, ":", 2)
- if len(vals[0]) == 0 {
+ if vals[0] == "" {
return "", 0, fmt.Errorf("failed to parse client IP: %v", req.RemoteAddr)
}
return vals[0], 1, nil
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/clock.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/clock_mutex.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/duration.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/duration_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/frozen.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/frozen_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/go19.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/interface.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/rfc822.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/rfc822_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/system.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/clock/system_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/collections/priority_queue.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/collections/priority_queue_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/collections/ttlmap.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/vulcand/oxy/internal/holsterv4/collections/ttlmap_test.go
No differences were encountered in the control files