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

More details

Full run details