├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── cmd └── kratgo │ └── main.go ├── config └── kratgo.conf.yml ├── docker ├── Dockerfile └── docker-entrypoint.sh ├── go.mod ├── go.sum ├── kratgo ├── const.go ├── kratgo.go ├── kratgo_test.go ├── types.go ├── utils.go └── utils_test.go └── modules ├── admin ├── admin.go ├── admin_test.go ├── http.go ├── http_test.go └── types.go ├── cache ├── cache.go ├── cache_test.go ├── cache_types.go ├── cache_types_gen.go ├── cache_types_gen_test.go ├── const.go ├── entry.go ├── entry_test.go ├── response.go ├── response_test.go └── types.go ├── config ├── const.go ├── parser.go ├── parser_test.go └── types.go ├── invalidator ├── const.go ├── entry.go ├── entry_test.go ├── errors.go ├── handler.go ├── handler_test.go ├── invalidator.go ├── invalidator_test.go └── types.go └── proxy ├── const.go ├── eval_params.go ├── eval_params_test.go ├── proxy.go ├── proxy_test.go ├── types.go ├── utils.go └── utils_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | go-version: [1.20.x] 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - run: go version 15 | - run: go get -t -v ./... 16 | - run: GOOS=linux go build ./... 17 | - run: GOOS=darwin go build ./... 18 | - run: GOOS=freebsd go build ./... 19 | - run: GOOS=windows go build ./... 20 | - run: GOARCH=386 go build ./... 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.20.x] 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - run: go version 15 | - run: go get -t -v ./... 16 | - run: go test -v -cover -race ./... 17 | 18 | - name: Send coverage 19 | uses: shogo82148/actions-goveralls@v1 20 | with: 21 | flag-name: Go-${{ matrix.go-version }} 22 | parallel: true 23 | 24 | finish: 25 | needs: test 26 | strategy: 27 | matrix: 28 | go-version: [1.20.x] 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: shogo82148/actions-goveralls@v1 32 | with: 33 | parallel-finished: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode/ 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Binary directory 18 | bin/ 19 | 20 | # Log files 21 | **/*.log 22 | 23 | # Develelop config file 24 | config/kratgo-dev.conf.yml 25 | 26 | # Profiling files 27 | **/*.prof 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all get build clean run 2 | .DEFAULT_GOAL: $(BIN_FILE) 3 | 4 | PROJECT_NAME = kratgo 5 | 6 | BIN_DIR = ./bin 7 | BIN_FILE = $(PROJECT_NAME) 8 | 9 | MODULES_DIR = ./modules 10 | 11 | KRATGO_DIR = $(PROJECT_NAME) 12 | CMD_DIR = ./cmd 13 | CONFIG_DIR = ./config/ 14 | 15 | # Get version constant 16 | VERSION := 1.1.9 17 | BUILD := $(shell git rev-parse HEAD) 18 | 19 | # Use linker flags to provide version/build settings to the binary 20 | LDFLAGS=-ldflags "-s -w -X=main.version=$(VERSION) -X=main.build=$(BUILD)" 21 | 22 | 23 | default: get build 24 | 25 | get: 26 | @echo "[*] Downloading dependencies..." 27 | cd $(CMD_DIR)/kratgo && go get 28 | @echo "[*] Finish..." 29 | 30 | vendor: 31 | @go mod vendor 32 | 33 | build: 34 | @echo "[*] Building $(PROJECT_NAME)..." 35 | go build $(LDFLAGS) -o $(BIN_DIR)/$(BIN_FILE) $(CMD_DIR)/... 36 | @echo "[*] Finish..." 37 | 38 | test: 39 | go test -v -race -cover ./... 40 | 41 | bench: 42 | go test -cpuprofile=cpu.prof -bench=. -benchmem $(MODULES_DIR)/proxy 43 | 44 | run: build 45 | $(BIN_DIR)/$(BIN_FILE) -config ./config/kratgo-dev.conf.yml 46 | 47 | install: 48 | mkdir -p /etc/kratgo/ 49 | cp $(BIN_DIR)/$(BIN_FILE) /usr/local/bin/ 50 | cp $(CONFIG_DIR)/kratgo.conf.yml /etc/kratgo/ 51 | 52 | uninstall: 53 | rm -rf /usr/local/bin/$(BIN_FILE) 54 | rm -rf /etc/kratgo/ 55 | 56 | clean: 57 | rm -rf bin/ 58 | rm -rf vendor/ 59 | 60 | docker_build: 61 | docker build -f ./docker/Dockerfile -t savsgio/kratgo . 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kratgo 2 | ====== 3 | 4 | [![Test status](https://github.com/savsgio/kratgo/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/savsgio/kratgo/actions?workflow=test) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/savsgio/kratgo)](https://goreportcard.com/report/github.com/savsgio/kratgo) 6 | [![GitHub release](https://img.shields.io/github/release/savsgio/kratgo.svg)](https://github.com/savsgio/kratgo/releases) 7 | [![Docker](https://img.shields.io/docker/automated/savsgio/kratgo.svg?colorB=blue&style=flat)](https://hub.docker.com/r/savsgio/kratgo) 8 | 9 | 10 | Simple, lightweight and ultra-fast HTTP Cache to speed up your websites. 11 | 12 | 13 | ### Requirements 14 | 15 | - [Go](https://golang.org/dl/) >= 1.20.X 16 | - make 17 | - git 18 | 19 | 20 | ## Features: 21 | 22 | - Cache proxy. 23 | - Load balancing beetwen backends. 24 | - Cache invalidation via API (Admin). 25 | - Configuration to non-cache certain requests. 26 | - Configuration to set or unset headers on especific requests. 27 | 28 | ## General 29 | 30 | To known if request pass across Kratgo Cache in backend servers, check the request header `X-Kratgo-Cache` with value `true`. 31 | 32 | 33 | ## Install 34 | 35 | Clone the repository: 36 | 37 | ```bash 38 | git clone https://github.com/savsgio/kratgo.git && cd kratgo 39 | ``` 40 | 41 | and execute: 42 | 43 | ```bash 44 | make 45 | make install 46 | ``` 47 | 48 | The binary file will install in `/usr/local/bin/kratgo` and configuration file in `/etc/kratgo/kratgo.conf.yml` 49 | 50 | 51 | ## Cache invalidation (Admin) 52 | 53 | The cache invalidation is available via API. The API's address is configured in ***admin*** section of the configuration file. 54 | 55 | This API only accepts ***POST*** requests with ***json***, under the path `/invalidate/`. 56 | 57 | Ex: `http://localhost:6082/invalidate/` 58 | 59 | The complete json body must be as following example: 60 | 61 | ```json 62 | { 63 | "host": "www.example.com", 64 | "path": "/es/", 65 | "header": { 66 | "key": "Content-Type", 67 | "value": "text/plain; charset=utf-8" 68 | } 69 | } 70 | ``` 71 | 72 | **IMPORTANT: All fields are optional, but at least you must specify one.** 73 | 74 | All invalidations will process by workers in Kratgo. You can configure the maximum available workers in the configuration. 75 | 76 | The workers are activated only when necessary. 77 | 78 | 79 | ## Docker 80 | 81 | The docker image is available in Docker Hub: [savsgio/kratgo](https://hub.docker.com/r/savsgio/kratgo) 82 | 83 | Get a basic configuration from [here](https://github.com/savsgio/kratgo/blob/master/config/kratgo.conf.yml) and customize it. 84 | 85 | Run with: 86 | 87 | ```bash 88 | docker run --rm --name kratgo -it -v -p 6081:6081 -p 6082:6082 savsgio/kratgo -config 89 | ``` 90 | 91 | ## Developers 92 | 93 | Copy configuration file `./config/kratgo.conf.yml` to `./config/kratgo-dev.conf.yml`, and customize it. 94 | 95 | Run with: 96 | 97 | ```bash 98 | make run 99 | ``` 100 | 101 | Contributing 102 | ------------ 103 | 104 | **Feel free to contribute it or fork me...** :wink: 105 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Cluster support with cache synchronization 2 | - Admin frontend: 3 | - Access authentication (include API) 4 | - Invalidate cache 5 | - Cache statistics 6 | - Server profiling (heap, gorutine, etc) 7 | -------------------------------------------------------------------------------- /cmd/kratgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/savsgio/kratgo/kratgo" 10 | "github.com/savsgio/kratgo/modules/config" 11 | ) 12 | 13 | var version, build, configFilePath string 14 | 15 | func init() { 16 | var showVersion bool 17 | flag.BoolVar(&showVersion, "version", false, "Print Kratgo version") 18 | 19 | flag.StringVar(&configFilePath, "config", "/etc/kratgo/kratgo.conf.yml", "Configuration file path") 20 | 21 | flag.Parse() 22 | 23 | if showVersion { 24 | fmt.Println("Kratgo:") 25 | fmt.Printf(" Version: %s\n", version) 26 | fmt.Printf(" Build: %s\n", build) 27 | fmt.Printf(" Runtime: %s\n", runtime.Version()) 28 | os.Exit(0) 29 | } 30 | } 31 | 32 | func main() { 33 | cfg, err := config.Parse(configFilePath) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | kratgo, err := kratgo.New(*cfg) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | if err = kratgo.ListenAndServe(); err != nil { 44 | panic(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/kratgo.conf.yml: -------------------------------------------------------------------------------- 1 | ########################### 2 | # KRATGO CONFIGURATION 3 | ########################### 4 | 5 | # IMPORTANT: Be careful with the tabulation indentation 6 | 7 | # --- Variables --- 8 | 9 | # $(method) : request method 10 | # $(host) : request host 11 | # $(path) : request path 12 | # $(contentType) : response backend's content type 13 | # $(statusCode) : response backend's status code 14 | # $(req.header::) : request header name 15 | # $(resp.header::) : response header name 16 | # $(cookie::) : request cookie name 17 | 18 | # --- Operators --- 19 | 20 | # Modifiers: + - / * & | ^ ** % >> << 21 | # Comparators: > >= < <= == != =~ !~ 22 | # Logical ops: || && 23 | # Numeric constants, as 64-bit floating point (12345.678) 24 | # String constants (single quotes: 'foobar') 25 | # Date constants (single quotes, using any permutation of RFC3339, ISO8601, ruby date, or unix date; date parsing is automatically tried with any string constant) 26 | # Boolean constants: true false 27 | # Parenthesis to control order of evaluation ( ) 28 | # Arrays (anything separated by , within parenthesis: (1, 2, 'foo')) 29 | # Prefixes: ! - ~ 30 | # Ternary conditional: ? : 31 | # Null coalescence: ?? 32 | 33 | # --- Log --- 34 | # Log level: fatal | error | warning | info | debug 35 | # Log output: 36 | # - console: Write output in standard error 37 | # - : Write output in log file 38 | 39 | logLevel: info 40 | logOutput: /var/log/kratgo/kratgo.log 41 | 42 | # --- Cache --- 43 | # ttl: Cache expiration in minutes 44 | # cleanFrequency: Interval in minutes between removing expired entries (clean up) 45 | # maxEntries: Max number of entries in cache. Used only to calculate initial size for cache 46 | # maxEntrySize: Max size of entry in bytes 47 | # hardMaxCacheSize: Limit for cache size in MB (Default value is 0 which means unlimited size) 48 | 49 | cache: 50 | ttl: 10 51 | cleanFrequency: 1 52 | maxEntries: 600000 53 | maxEntrySize: 500 54 | hardMaxCacheSize: 0 55 | 56 | # --- Invalidator --- 57 | # maxWorkers: Maximum workers to execute invalidations 58 | 59 | invalidator: 60 | maxWorkers: 5 61 | 62 | # --- Proxy --- 63 | # addr: IP and Port of Kratgo 64 | # backendAddrs: Array with "addr:port" of the backends 65 | # response: Configuration to manipulate reponse (Optional) 66 | # headers: 67 | # set: Configuration to SET headers from response (Optional) 68 | # - name: Header name 69 | # value: Value of header 70 | # if: Condition to set this header (Optional) 71 | # 72 | # unset: Configuration to UNSET headers from response (Optional) 73 | # - name: Header name 74 | # if: Condition to unset this header (Optional) 75 | # 76 | # nocache: Conditions to not save in cache the backend response (Optional) 77 | 78 | proxy: 79 | addr: 0.0.0.0:6081 80 | backendAddrs: 81 | [ 82 | :, 83 | ] 84 | response: 85 | headers: 86 | set: 87 | - name: X-Kratgo 88 | value: true 89 | 90 | unset: 91 | - name: Set-Cookie 92 | if: $(req.header::X-Requested-With) != 'XMLHttpRequest' 93 | 94 | nocache: 95 | - $(req.header::X-Requested-With) == 'XMLHttpRequest' 96 | 97 | # --- Admin --- 98 | # addr: IP and Port of admin api 99 | 100 | admin: 101 | addr: 0.0.0.0:6082 102 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ############### 2 | ### BUILDER ### 3 | ############### 4 | 5 | FROM golang:1.20-alpine3.17 as builder 6 | 7 | RUN apk add git build-base 8 | 9 | RUN mkdir -p /usr/src/kratgo 10 | WORKDIR /usr/src/kratgo 11 | 12 | ADD . . 13 | 14 | RUN make 15 | 16 | ############### 17 | ### RELEASE ### 18 | ############### 19 | 20 | FROM alpine:3.17 21 | 22 | LABEL Author="Sergio Andres Virviescas Santana " 23 | 24 | COPY --from=builder /usr/src/kratgo/ /kratgo 25 | 26 | RUN cd /kratgo \ 27 | && apk add make git \ 28 | && make install \ 29 | && rm -rf /kratgo \ 30 | && apk del make git 31 | 32 | # Configuration 33 | COPY ./docker/docker-entrypoint.sh /usr/local/bin/ 34 | RUN ln -s /usr/local/bin/docker-entrypoint.sh /entrypoint.sh # backwards compat 35 | ENTRYPOINT ["docker-entrypoint.sh"] 36 | 37 | CMD ["kratgo"] 38 | 39 | EXPOSE 6081 6082 40 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # first arg is `-config` or `--some-option` 6 | if [ "${1#-}" != "$1" ]; then 7 | set -- kratgo "$@" 8 | fi 9 | 10 | exec "$@" 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/savsgio/kratgo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/allegro/bigcache/v3 v3.1.0 7 | github.com/savsgio/atreugo/v11 v11.9.12 8 | github.com/savsgio/go-logger/v4 v4.2.0 9 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee 10 | github.com/savsgio/govaluate/v3 v3.0.0 11 | github.com/tinylib/msgp v1.1.8 12 | github.com/valyala/fasthttp v1.45.0 13 | gopkg.in/yaml.v2 v2.4.0 14 | ) 15 | 16 | require ( 17 | github.com/andybalholm/brotli v1.0.5 // indirect 18 | github.com/fasthttp/router v1.4.18 // indirect 19 | github.com/klauspost/compress v1.16.3 // indirect 20 | github.com/philhofer/fwd v1.1.2 // indirect 21 | github.com/valyala/bytebufferpool v1.0.0 // indirect 22 | github.com/valyala/tcplisten v1.0.0 // indirect 23 | golang.org/x/sys v0.6.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= 2 | github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= 3 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 4 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 5 | github.com/atreugo/mock v0.0.0-20200601091009-13c275b330b0 h1:IVqe9WnancrkICl5HqEfGjrnkQ4+VsU5fodcuFVoG/A= 6 | github.com/fasthttp/router v1.4.18 h1:elMnlFq527oZd8MHsuUpO6uLDup1exv8rXPfIjClDHk= 7 | github.com/fasthttp/router v1.4.18/go.mod h1:ZmC20Mn0VgCBbUWFDmnYzFbQYRfdGeKgpkBy0+JioKA= 8 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= 10 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 11 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 12 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 13 | github.com/savsgio/atreugo/v11 v11.9.12 h1:2U2lJkKKBvH50TRE8Ik/92WLen5VytfTnVLsPnUUvLQ= 14 | github.com/savsgio/atreugo/v11 v11.9.12/go.mod h1:fHgbSApU3+VlQVwMtMYLQsJ2LLchPwhmuP0TZ1KJdGk= 15 | github.com/savsgio/go-logger/v4 v4.2.0 h1:m3PtyEpfAzn9H/VUcipEm6ToIdEtuIv808Sh0v08bVw= 16 | github.com/savsgio/go-logger/v4 v4.2.0/go.mod h1:pmdGWSa4ZuHGqETpZH6ZRRFMkZdFffCphvrlmGhchvI= 17 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= 18 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 19 | github.com/savsgio/govaluate/v3 v3.0.0 h1:nilYfq6DyMQvy/++E42NW+s56k0I5jgqjm5IRtrwRfc= 20 | github.com/savsgio/govaluate/v3 v3.0.0/go.mod h1:4hnQJKZBjiFt7RfuNjx5tmXs1yL6BhtP5j4OmZHm4C0= 21 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 22 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 23 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 24 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 25 | github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA= 26 | github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 27 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 28 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 29 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 30 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 31 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 32 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 33 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 34 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 35 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 36 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 37 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 38 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 48 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 50 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 51 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 52 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 53 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 54 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 55 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 58 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 59 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 60 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 64 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 65 | -------------------------------------------------------------------------------- /kratgo/const.go: -------------------------------------------------------------------------------- 1 | package kratgo 2 | 3 | const defaultLogOutput = "console" 4 | 5 | const defaultHTTPScheme = "http" 6 | const httpSchemeTLS = "https" 7 | -------------------------------------------------------------------------------- /kratgo/kratgo.go: -------------------------------------------------------------------------------- 1 | package kratgo 2 | 3 | import ( 4 | "github.com/savsgio/go-logger/v4" 5 | "github.com/savsgio/kratgo/modules/admin" 6 | "github.com/savsgio/kratgo/modules/cache" 7 | "github.com/savsgio/kratgo/modules/config" 8 | "github.com/savsgio/kratgo/modules/invalidator" 9 | "github.com/savsgio/kratgo/modules/proxy" 10 | ) 11 | 12 | // New ... 13 | func New(cfg config.Config) (*Kratgo, error) { 14 | k := new(Kratgo) 15 | 16 | logLevel, err := logger.ParseLevel(cfg.LogLevel) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | logFile, err := getLogOutput(cfg.LogOutput) 22 | if err != nil { 23 | return nil, err 24 | } 25 | k.logFile = logFile 26 | 27 | c, err := cache.New(cache.Config{ 28 | FileConfig: cfg.Cache, 29 | LogLevel: logLevel, 30 | LogOutput: logFile, 31 | }) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | if k.Proxy, err = proxy.New(proxy.Config{ 37 | FileConfig: cfg.Proxy, 38 | Cache: c, 39 | HTTPScheme: defaultHTTPScheme, 40 | LogLevel: logLevel, 41 | LogOutput: logFile, 42 | }); err != nil { 43 | return nil, err 44 | } 45 | 46 | i, err := invalidator.New(invalidator.Config{ 47 | FileConfig: cfg.Invalidator, 48 | Cache: c, 49 | LogLevel: logLevel, 50 | LogOutput: logFile, 51 | }) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if k.Admin, err = admin.New(admin.Config{ 57 | FileConfig: cfg.Admin, 58 | Cache: c, 59 | Invalidator: i, 60 | HTTPScheme: defaultHTTPScheme, 61 | LogLevel: logLevel, 62 | LogOutput: logFile, 63 | }); err != nil { 64 | return nil, err 65 | } 66 | 67 | return k, nil 68 | } 69 | 70 | // ListenAndServe ... 71 | func (k *Kratgo) ListenAndServe() error { 72 | defer k.logFile.Close() 73 | 74 | err := make(chan error, 1) 75 | 76 | go func() { 77 | err <- k.Admin.ListenAndServe() 78 | }() 79 | 80 | go func() { 81 | err <- k.Proxy.ListenAndServe() 82 | }() 83 | 84 | return <-err 85 | } 86 | -------------------------------------------------------------------------------- /kratgo/kratgo_test.go: -------------------------------------------------------------------------------- 1 | package kratgo 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | logger "github.com/savsgio/go-logger/v4" 9 | "github.com/savsgio/kratgo/modules/config" 10 | ) 11 | 12 | type mockServer struct { 13 | listenAndServeCalled bool 14 | mu sync.RWMutex 15 | } 16 | 17 | func (mock *mockServer) ListenAndServe() error { 18 | mock.mu.Lock() 19 | mock.listenAndServeCalled = true 20 | mock.mu.Unlock() 21 | 22 | time.Sleep(250 * time.Millisecond) 23 | 24 | return nil 25 | } 26 | 27 | func TestKratgo_New(t *testing.T) { 28 | type args struct { 29 | cfg config.Config 30 | } 31 | 32 | type want struct { 33 | logFileName string 34 | err bool 35 | } 36 | 37 | logFileName := "/tmp/test_kratgo.log" 38 | logLevel := logger.FATAL.String() 39 | 40 | cfgAdmin := config.Admin{ 41 | Addr: "localhost:9999", 42 | } 43 | cfgCache := config.Cache{ 44 | TTL: 10, 45 | CleanFrequency: 5, 46 | MaxEntries: 5, 47 | MaxEntrySize: 20, 48 | HardMaxCacheSize: 30, 49 | } 50 | cfgInvalidator := config.Invalidator{ 51 | MaxWorkers: 1, 52 | } 53 | cfgProxy := config.Proxy{ 54 | Addr: "localhost:8000", 55 | BackendAddrs: []string{"localhost:9990", "localhost:9991", "localhost:9993", "localhost:9994"}, 56 | } 57 | 58 | tests := []struct { 59 | name string 60 | args args 61 | want want 62 | }{ 63 | { 64 | name: "Ok", 65 | args: args{ 66 | cfg: config.Config{ 67 | Admin: cfgAdmin, 68 | Cache: cfgCache, 69 | Invalidator: cfgInvalidator, 70 | Proxy: cfgProxy, 71 | LogLevel: logLevel, 72 | LogOutput: logFileName, 73 | }, 74 | }, 75 | want: want{ 76 | logFileName: logFileName, 77 | err: false, 78 | }, 79 | }, 80 | // { 81 | // name: "InvalidAdmin", 82 | // args: args{ 83 | // cfg: config.Config{ 84 | // Cache: cfgCache, 85 | // Invalidator: cfgInvalidator, 86 | // Proxy: cfgProxy, 87 | // LogLevel: logLevel, 88 | // LogOutput: logFileName, 89 | // }, 90 | // }, 91 | // want: want{ 92 | // err: true, 93 | // }, 94 | // }, 95 | { 96 | name: "InvalidCache", 97 | args: args{ 98 | cfg: config.Config{ 99 | Admin: cfgAdmin, 100 | Invalidator: cfgInvalidator, 101 | Proxy: cfgProxy, 102 | LogLevel: logLevel, 103 | LogOutput: logFileName, 104 | }, 105 | }, 106 | want: want{ 107 | err: true, 108 | }, 109 | }, 110 | { 111 | name: "InvalidInvalidator", 112 | args: args{ 113 | cfg: config.Config{ 114 | Admin: cfgAdmin, 115 | Cache: cfgCache, 116 | Proxy: cfgProxy, 117 | LogLevel: logLevel, 118 | LogOutput: logFileName, 119 | }, 120 | }, 121 | want: want{ 122 | err: true, 123 | }, 124 | }, 125 | { 126 | name: "InvalidProxy", 127 | args: args{ 128 | cfg: config.Config{ 129 | Admin: cfgAdmin, 130 | Cache: cfgCache, 131 | Invalidator: cfgInvalidator, 132 | LogLevel: logLevel, 133 | LogOutput: logFileName, 134 | }, 135 | }, 136 | want: want{ 137 | err: true, 138 | }, 139 | }, 140 | { 141 | name: "InvalidLogOutput", 142 | args: args{ 143 | cfg: config.Config{ 144 | Admin: cfgAdmin, 145 | Cache: cfgCache, 146 | Invalidator: cfgInvalidator, 147 | Proxy: cfgProxy, 148 | LogLevel: logLevel, 149 | }, 150 | }, 151 | want: want{ 152 | err: true, 153 | }, 154 | }, 155 | } 156 | 157 | for _, tt := range tests { 158 | t.Run(tt.name, func(t *testing.T) { 159 | k, err := New(tt.args.cfg) 160 | if (err != nil) != tt.want.err { 161 | t.Fatalf("New() error == '%v', want '%v'", err, tt.want.err) 162 | } 163 | 164 | if tt.want.err { 165 | return 166 | } 167 | 168 | logName := k.logFile.Name() 169 | if logName != tt.want.logFileName { 170 | t.Errorf("Kratgo.New() log file == '%s', want '%s'", logName, tt.want.logFileName) 171 | } 172 | 173 | if k.Admin == nil { 174 | t.Errorf("Kratgo.New() Admin is '%v'", nil) 175 | } 176 | if k.Proxy == nil { 177 | t.Errorf("Kratgo.New() Proxy is '%v'", nil) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func TestKratgo_ListenAndServe(t *testing.T) { 184 | proxyMock := new(mockServer) 185 | adminMock := new(mockServer) 186 | 187 | k := new(Kratgo) 188 | k.Proxy = proxyMock 189 | k.Admin = adminMock 190 | 191 | k.ListenAndServe() 192 | 193 | // Sleep to wait the gorutine start 194 | time.Sleep(500 * time.Millisecond) 195 | 196 | proxyMock.mu.RLock() 197 | defer proxyMock.mu.RUnlock() 198 | if !proxyMock.listenAndServeCalled { 199 | t.Error("Kratgo.ListenAndServe() proxy server is not listening") 200 | } 201 | 202 | adminMock.mu.RLock() 203 | defer adminMock.mu.RUnlock() 204 | if !adminMock.listenAndServeCalled { 205 | t.Error("Kratgo.ListenAndServe() admin server is not listening") 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /kratgo/types.go: -------------------------------------------------------------------------------- 1 | package kratgo 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Kratgo ... 8 | type Kratgo struct { 9 | Proxy Server 10 | Admin Server 11 | 12 | logFile *os.File 13 | } 14 | 15 | // ###### INTERFACES ###### 16 | 17 | // Server ... 18 | type Server interface { 19 | ListenAndServe() error 20 | } 21 | -------------------------------------------------------------------------------- /kratgo/utils.go: -------------------------------------------------------------------------------- 1 | package kratgo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | ) 8 | 9 | func getLogOutput(output string) (*os.File, error) { 10 | if output == "" { 11 | return nil, fmt.Errorf("Invalid log output: '%s'", output) 12 | 13 | } else if output == "console" { 14 | return os.Stderr, nil 15 | } 16 | 17 | dirPath, _ := path.Split(output) 18 | if err := os.MkdirAll(dirPath, os.ModeDir); err != nil { 19 | return nil, err 20 | } 21 | return os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0755) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /kratgo/utils_test.go: -------------------------------------------------------------------------------- 1 | package kratgo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_getLogOutput(t *testing.T) { 8 | type args struct { 9 | output string 10 | } 11 | 12 | type want struct { 13 | fileName string 14 | err bool 15 | } 16 | 17 | tests := []struct { 18 | name string 19 | args args 20 | want want 21 | }{ 22 | { 23 | name: "console", 24 | args: args{output: defaultLogOutput}, 25 | want: want{ 26 | fileName: "/dev/stderr", 27 | err: false, 28 | }, 29 | }, 30 | { 31 | name: "file", 32 | args: args{output: "/tmp/kratgo_test.log"}, 33 | want: want{ 34 | fileName: "/tmp/kratgo_test.log", 35 | err: false, 36 | }, 37 | }, 38 | { 39 | name: "invalid", 40 | args: args{output: ""}, 41 | want: want{ 42 | err: true, 43 | }, 44 | }, 45 | { 46 | name: "error", 47 | args: args{output: "/sadasdadr2343dcr4c234/kratgo_test.log"}, 48 | want: want{ 49 | err: true, 50 | }, 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | f, err := getLogOutput(tt.args.output) 56 | if (err != nil) != tt.want.err { 57 | t.Fatalf("getLogOutput() unexpected error: %v", err) 58 | } 59 | 60 | if f != nil { 61 | fileName := f.Name() 62 | if fileName != tt.want.fileName { 63 | t.Errorf("getLogOutput() file = '%s', want '%s'", fileName, tt.want.fileName) 64 | } 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/savsgio/atreugo/v11" 5 | logger "github.com/savsgio/go-logger/v4" 6 | ) 7 | 8 | // New ... 9 | func New(cfg Config) (*Admin, error) { 10 | a := new(Admin) 11 | a.fileConfig = cfg.FileConfig 12 | 13 | log := logger.New(cfg.LogLevel, cfg.LogOutput, logger.Field{Key: "type", Value: "admin"}) 14 | 15 | a.server = atreugo.New(atreugo.Config{ 16 | Addr: cfg.FileConfig.Addr, 17 | Logger: log, 18 | }) 19 | 20 | a.httpScheme = cfg.HTTPScheme 21 | a.cache = cfg.Cache 22 | a.invalidator = cfg.Invalidator 23 | a.log = log 24 | 25 | a.init() 26 | 27 | return a, nil 28 | } 29 | 30 | func (a *Admin) init() { 31 | a.server.Path("POST", "/invalidate/", a.invalidateView) 32 | } 33 | 34 | // ListenAndServe ... 35 | func (a *Admin) ListenAndServe() error { 36 | go a.invalidator.Start() 37 | 38 | return a.server.ListenAndServe() 39 | } 40 | -------------------------------------------------------------------------------- /modules/admin/admin_test.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "reflect" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/savsgio/atreugo/v11" 12 | logger "github.com/savsgio/go-logger/v4" 13 | "github.com/savsgio/kratgo/modules/cache" 14 | "github.com/savsgio/kratgo/modules/config" 15 | "github.com/savsgio/kratgo/modules/invalidator" 16 | ) 17 | 18 | var testCache *cache.Cache 19 | 20 | func init() { 21 | c, err := cache.New(cache.Config{ 22 | FileConfig: fileConfigCache(), 23 | LogLevel: logger.ERROR, 24 | LogOutput: os.Stderr, 25 | }) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | testCache = c 31 | } 32 | 33 | type mockPath struct { 34 | method string 35 | url string 36 | view atreugo.View 37 | } 38 | 39 | type mockServer struct { 40 | listenAndServeCalled bool 41 | logOutput io.Writer 42 | 43 | paths []mockPath 44 | 45 | mu sync.RWMutex 46 | } 47 | 48 | func (mock *mockServer) ListenAndServe() error { 49 | mock.mu.Lock() 50 | mock.listenAndServeCalled = true 51 | mock.mu.Unlock() 52 | 53 | time.Sleep(250 * time.Millisecond) 54 | 55 | return nil 56 | } 57 | 58 | func (mock *mockServer) Path(httpMethod string, url string, viewFn atreugo.View) *atreugo.Path { 59 | mock.paths = append(mock.paths, mockPath{ 60 | method: httpMethod, 61 | url: url, 62 | view: viewFn, 63 | }) 64 | 65 | return nil 66 | } 67 | 68 | func (mock *mockServer) SetLogOutput(output io.Writer) { 69 | mock.logOutput = output 70 | } 71 | 72 | type mockInvalidator struct { 73 | addCalled bool 74 | startCalled bool 75 | err error 76 | 77 | mu sync.RWMutex 78 | } 79 | 80 | func (mock *mockInvalidator) Start() { 81 | mock.mu.Lock() 82 | mock.startCalled = true 83 | mock.mu.Unlock() 84 | } 85 | 86 | func (mock *mockInvalidator) Add(e invalidator.Entry) error { 87 | mock.mu.Lock() 88 | mock.addCalled = true 89 | mock.mu.Unlock() 90 | 91 | return mock.err 92 | } 93 | 94 | func getMockPath(paths []mockPath, url, method string) *mockPath { 95 | for _, v := range paths { 96 | if v.url == url && v.method == method { 97 | return &v 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func fileConfigAdmin() config.Admin { 105 | return config.Admin{ 106 | Addr: "localhost:9999", 107 | } 108 | } 109 | 110 | func fileConfigCache() config.Cache { 111 | return config.Cache{ 112 | TTL: 10, 113 | CleanFrequency: 5, 114 | MaxEntries: 5, 115 | MaxEntrySize: 20, 116 | HardMaxCacheSize: 30, 117 | } 118 | } 119 | 120 | func testConfig() Config { 121 | testCache.Reset() 122 | 123 | return Config{ 124 | FileConfig: fileConfigAdmin(), 125 | Cache: testCache, 126 | Invalidator: nil, 127 | HTTPScheme: "http", 128 | LogLevel: logger.FATAL, 129 | LogOutput: os.Stderr, 130 | } 131 | } 132 | 133 | func TestAdmin_New(t *testing.T) { 134 | type args struct { 135 | cfg Config 136 | } 137 | 138 | type want struct { 139 | err bool 140 | } 141 | 142 | logLevel := logger.FATAL 143 | logOutput := os.Stderr 144 | httpScheme := "http" 145 | invalidatorMock := new(mockInvalidator) 146 | 147 | tests := []struct { 148 | name string 149 | args args 150 | want want 151 | }{ 152 | { 153 | name: "Ok", 154 | args: args{ 155 | cfg: Config{ 156 | FileConfig: config.Admin{ 157 | Addr: "localhost:9999", 158 | }, 159 | Cache: testCache, 160 | Invalidator: invalidatorMock, 161 | HTTPScheme: httpScheme, 162 | LogLevel: logLevel, 163 | LogOutput: logOutput, 164 | }, 165 | }, 166 | want: want{ 167 | err: false, 168 | }, 169 | }, 170 | } 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | a, err := New(tt.args.cfg) 174 | if (err != nil) != tt.want.err { 175 | t.Fatalf("New() error == '%v', want '%v'", err, tt.want.err) 176 | } 177 | 178 | if tt.want.err { 179 | return 180 | } 181 | 182 | if a.server == nil { 183 | t.Errorf("New() server is '%v'", nil) 184 | } 185 | 186 | if a.httpScheme != httpScheme { 187 | t.Errorf("New() httpScheme == '%s', want '%s'", a.httpScheme, httpScheme) 188 | } 189 | 190 | adminCachePtr := reflect.ValueOf(a.cache).Pointer() 191 | testCachePtr := reflect.ValueOf(testCache).Pointer() 192 | if adminCachePtr != testCachePtr { 193 | t.Errorf("New() cache == '%d', want '%d'", adminCachePtr, testCachePtr) 194 | } 195 | 196 | adminInvalidatorPtr := reflect.ValueOf(a.invalidator).Pointer() 197 | invalidatorPtr := reflect.ValueOf(invalidatorMock).Pointer() 198 | if adminInvalidatorPtr != invalidatorPtr { 199 | t.Errorf("New() invalidator == '%d', want '%d'", adminInvalidatorPtr, invalidatorPtr) 200 | } 201 | 202 | if a.log == nil { 203 | t.Errorf("New() log is '%v'", nil) 204 | } 205 | }) 206 | } 207 | } 208 | 209 | func TestAdmin_init(t *testing.T) { 210 | serverMock := new(mockServer) 211 | 212 | admin := new(Admin) 213 | admin.server = serverMock 214 | admin.init() 215 | 216 | expectedPaths := []mockPath{ 217 | { 218 | method: "POST", 219 | url: "/invalidate/", 220 | view: admin.invalidateView, 221 | }, 222 | } 223 | 224 | if len(expectedPaths) != len(serverMock.paths) { 225 | t.Fatalf("Admin.server.init() registered paths == '%v', want '%v'", serverMock.paths, expectedPaths) 226 | } 227 | 228 | for _, path := range serverMock.paths { 229 | p := getMockPath(expectedPaths, path.url, path.method) 230 | if p == nil { 231 | t.Errorf("Admin.server.path() method == '%s', want '%s'", path.method, p.method) 232 | t.Errorf("Admin.server.path() url == '%s', want '%s'", path.url, p.url) 233 | 234 | } else { 235 | if reflect.ValueOf(path.view).Pointer() != reflect.ValueOf(p.view).Pointer() { 236 | t.Errorf("Admin.server.path() url == '%p', want '%p'", path.view, p.view) 237 | } 238 | } 239 | } 240 | 241 | } 242 | 243 | func TestAdmin_ListenAndServe(t *testing.T) { 244 | serverMock := new(mockServer) 245 | invalidatorMock := new(mockInvalidator) 246 | 247 | admin := new(Admin) 248 | admin.server = serverMock 249 | admin.invalidator = invalidatorMock 250 | 251 | admin.ListenAndServe() 252 | 253 | invalidatorMock.mu.RLock() 254 | defer invalidatorMock.mu.RUnlock() 255 | if !invalidatorMock.startCalled { 256 | t.Error("Admin.ListenAndServe() invalidator is not start") 257 | } 258 | 259 | serverMock.mu.RLock() 260 | defer serverMock.mu.RUnlock() 261 | if !serverMock.listenAndServeCalled { 262 | t.Error("Admin.ListenAndServe() server is not listening") 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /modules/admin/http.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/savsgio/atreugo/v11" 7 | "github.com/savsgio/kratgo/modules/invalidator" 8 | ) 9 | 10 | func (a *Admin) invalidateView(ctx *atreugo.RequestCtx) error { 11 | entry := invalidator.AcquireEntry() 12 | body := ctx.PostBody() 13 | 14 | a.log.Debugf("Invalidation received: %s", body) 15 | 16 | err := json.Unmarshal(body, entry) 17 | if err != nil { 18 | invalidator.ReleaseEntry(entry) 19 | return err 20 | } 21 | 22 | if err = a.invalidator.Add(*entry); err != nil { 23 | a.log.Errorf("Could not add a invalidation entry '%s': %v", body, err) 24 | invalidator.ReleaseEntry(entry) 25 | return ctx.TextResponse(err.Error(), 400) 26 | } 27 | 28 | invalidator.ReleaseEntry(entry) 29 | 30 | return ctx.TextResponse("OK") 31 | } 32 | -------------------------------------------------------------------------------- /modules/admin/http_test.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/savsgio/atreugo/v11" 7 | "github.com/savsgio/kratgo/modules/invalidator" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | func TestAdmin_invalidateView(t *testing.T) { 12 | type args struct { 13 | method string 14 | body string 15 | addError error 16 | } 17 | 18 | type want struct { 19 | response string 20 | statusCode int 21 | err bool 22 | callAdd bool 23 | } 24 | 25 | tests := []struct { 26 | name string 27 | args args 28 | want want 29 | }{ 30 | { 31 | name: "Ok", 32 | args: args{ 33 | method: "POST", 34 | body: "{\"host\": \"www.kratgo.com\"}", 35 | }, 36 | want: want{ 37 | response: "OK", 38 | statusCode: 200, 39 | err: false, 40 | callAdd: true, 41 | }, 42 | }, 43 | { 44 | name: "EmptyJSONBody", 45 | args: args{ 46 | method: "POST", 47 | body: "{}", 48 | addError: invalidator.ErrEmptyFields, 49 | }, 50 | want: want{ 51 | response: invalidator.ErrEmptyFields.Error(), 52 | statusCode: 400, 53 | err: false, 54 | callAdd: false, 55 | }, 56 | }, 57 | { 58 | name: "InvalidJSONBody", 59 | args: args{ 60 | method: "POST", 61 | body: "\"", 62 | }, 63 | want: want{ 64 | response: "", // err message is setted by Atreugo when is returned the error 65 | statusCode: 200, // 500 is setted by Atreugo when is returned the error 66 | err: true, 67 | callAdd: false, 68 | }, 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | invalidatorMock := &mockInvalidator{ 75 | err: tt.args.addError, 76 | } 77 | 78 | admin, err := New(testConfig()) 79 | if err != nil { 80 | t.Fatalf("Unexpected error: %v", err) 81 | } 82 | admin.invalidator = invalidatorMock 83 | 84 | actx := new(atreugo.RequestCtx) 85 | actx.RequestCtx = new(fasthttp.RequestCtx) 86 | 87 | actx.Request.Header.SetMethod(tt.args.method) 88 | actx.Request.SetBodyString(tt.args.body) 89 | 90 | err = admin.invalidateView(actx) 91 | if (err != nil) != tt.want.err { 92 | t.Fatalf("Admin.invalidateView() error == '%v', want '%v'", err, tt.want.err) 93 | } 94 | 95 | if tt.want.callAdd && !invalidatorMock.addCalled { 96 | t.Error("Admin.invalidateView() has not called to admin.invalidator.Add(...)") 97 | } 98 | 99 | statusCode := actx.Response.StatusCode() 100 | if statusCode != tt.want.statusCode { 101 | t.Errorf("Admin.invalidateView() status code == '%d', want '%d'", statusCode, tt.want.statusCode) 102 | } 103 | 104 | respBody := string(actx.Response.Body()) 105 | if respBody != tt.want.response { 106 | t.Errorf("Admin.invalidateView() response body == '%s', want '%s'", respBody, tt.want.response) 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /modules/admin/types.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/savsgio/atreugo/v11" 7 | logger "github.com/savsgio/go-logger/v4" 8 | "github.com/savsgio/kratgo/modules/cache" 9 | "github.com/savsgio/kratgo/modules/config" 10 | "github.com/savsgio/kratgo/modules/invalidator" 11 | ) 12 | 13 | // Config ... 14 | type Config struct { 15 | FileConfig config.Admin 16 | Cache *cache.Cache 17 | Invalidator Invalidator 18 | 19 | HTTPScheme string 20 | 21 | LogLevel logger.Level 22 | LogOutput io.Writer 23 | } 24 | 25 | // Admin ... 26 | type Admin struct { 27 | fileConfig config.Admin 28 | 29 | server Server 30 | cache *cache.Cache 31 | invalidator Invalidator 32 | 33 | httpScheme string 34 | 35 | log *logger.Logger 36 | } 37 | 38 | // ###### INTERFACES ###### 39 | 40 | // Invalidator ... 41 | type Invalidator interface { 42 | Start() 43 | Add(e invalidator.Entry) error 44 | } 45 | 46 | // Server ... 47 | type Server interface { 48 | ListenAndServe() error 49 | Path(httpMethod string, url string, viewFn atreugo.View) *atreugo.Path 50 | } 51 | -------------------------------------------------------------------------------- /modules/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/allegro/bigcache/v3" 8 | logger "github.com/savsgio/go-logger/v4" 9 | "github.com/savsgio/gotils/strconv" 10 | "github.com/savsgio/kratgo/modules/config" 11 | ) 12 | 13 | func bigcacheConfig(cfg config.Cache) bigcache.Config { 14 | return bigcache.Config{ 15 | Shards: defaultBigcacheShards, 16 | LifeWindow: time.Duration(cfg.TTL) * time.Minute, 17 | CleanWindow: time.Duration(cfg.CleanFrequency) * time.Minute, 18 | MaxEntriesInWindow: cfg.MaxEntries, 19 | MaxEntrySize: cfg.MaxEntrySize, 20 | Verbose: false, 21 | HardMaxCacheSize: cfg.HardMaxCacheSize, 22 | } 23 | } 24 | 25 | // New ... 26 | func New(cfg Config) (*Cache, error) { 27 | if cfg.FileConfig.CleanFrequency == 0 { 28 | return nil, fmt.Errorf("Cache.CleanFrequency configuration must be greater than 0") 29 | } 30 | 31 | c := new(Cache) 32 | c.fileConfig = cfg.FileConfig 33 | 34 | log := logger.New(cfg.LogLevel, cfg.LogOutput, logger.Field{Key: "type", Value: "cache"}) 35 | 36 | bigcacheCFG := bigcacheConfig(c.fileConfig) 37 | bigcacheCFG.Logger = log 38 | bigcacheCFG.Verbose = cfg.LogLevel == logger.DEBUG 39 | 40 | c.bc, _ = bigcache.NewBigCache(bigcacheCFG) 41 | 42 | return c, nil 43 | } 44 | 45 | // Set ... 46 | func (c *Cache) Set(key string, entry Entry) error { 47 | data, _ := Marshal(entry) 48 | 49 | return c.bc.Set(key, data) 50 | } 51 | 52 | // SetBytes ... 53 | func (c *Cache) SetBytes(key []byte, entry Entry) error { 54 | return c.Set(strconv.B2S(key), entry) 55 | } 56 | 57 | // Get ... 58 | func (c *Cache) Get(key string, dst *Entry) error { 59 | data, err := c.bc.Get(key) 60 | if err == bigcache.ErrEntryNotFound { 61 | return nil 62 | } else if err != nil { 63 | return err 64 | } 65 | 66 | return Unmarshal(dst, data) 67 | } 68 | 69 | // GetBytes ... 70 | func (c *Cache) GetBytes(key []byte, dst *Entry) error { 71 | return c.Get(strconv.B2S(key), dst) 72 | } 73 | 74 | // Del ... 75 | func (c *Cache) Del(key string) error { 76 | return c.bc.Delete(key) 77 | } 78 | 79 | // DelBytes ... 80 | func (c *Cache) DelBytes(key []byte) error { 81 | return c.Del(strconv.B2S(key)) 82 | } 83 | 84 | // Iterator ... 85 | func (c *Cache) Iterator() *bigcache.EntryInfoIterator { 86 | return c.bc.Iterator() 87 | } 88 | 89 | // Len ... 90 | func (c *Cache) Len() int { 91 | return c.bc.Len() 92 | } 93 | 94 | // Reset ... 95 | func (c *Cache) Reset() error { 96 | return c.bc.Reset() 97 | } 98 | -------------------------------------------------------------------------------- /modules/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | logger "github.com/savsgio/go-logger/v4" 10 | "github.com/savsgio/kratgo/modules/config" 11 | ) 12 | 13 | var testCache *Cache 14 | 15 | func init() { 16 | c, err := New(Config{ 17 | FileConfig: fileConfigCache(), 18 | LogLevel: logger.ERROR, 19 | LogOutput: os.Stderr, 20 | }) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | testCache = c 26 | } 27 | 28 | func fileConfigCache() config.Cache { 29 | return config.Cache{ 30 | TTL: 10, 31 | CleanFrequency: 5, 32 | MaxEntries: 5, 33 | MaxEntrySize: 20, 34 | HardMaxCacheSize: 30, 35 | } 36 | } 37 | 38 | func Test_bigcacheConfig(t *testing.T) { 39 | cfg := fileConfigCache() 40 | bcConfig := bigcacheConfig(cfg) 41 | 42 | if bcConfig.Shards != defaultBigcacheShards { 43 | t.Errorf("bigcacheConfig() Shards == '%d', want '%d'", bcConfig.Shards, defaultBigcacheShards) 44 | } 45 | 46 | lifeWindoow := time.Duration(cfg.TTL) * time.Minute 47 | if bcConfig.LifeWindow != lifeWindoow { 48 | t.Errorf("bigcacheConfig() LifeWindow == '%d', want '%d'", bcConfig.LifeWindow, lifeWindoow) 49 | } 50 | 51 | cleanWindow := time.Duration(cfg.CleanFrequency) * time.Minute 52 | if bcConfig.CleanWindow != cleanWindow { 53 | t.Errorf("bigcacheConfig() CleanWindow == '%d', want '%d'", bcConfig.CleanWindow, cleanWindow) 54 | } 55 | 56 | maxEntriesInWindow := cfg.MaxEntries 57 | if bcConfig.MaxEntriesInWindow != maxEntriesInWindow { 58 | t.Errorf("bigcacheConfig() MaxEntriesInWindow == '%d', want '%d'", bcConfig.MaxEntriesInWindow, maxEntriesInWindow) 59 | } 60 | 61 | maxEntriesSize := cfg.MaxEntrySize 62 | if bcConfig.MaxEntrySize != maxEntriesSize { 63 | t.Errorf("bigcacheConfig() MaxEntrySize == '%d', want '%d'", bcConfig.MaxEntrySize, maxEntriesSize) 64 | } 65 | 66 | verbose := false 67 | if bcConfig.Verbose != verbose { 68 | t.Errorf("bigcacheConfig() Verbose == '%v', want '%v'", bcConfig.Verbose, verbose) 69 | } 70 | 71 | hardMaxCacheSize := cfg.HardMaxCacheSize 72 | if bcConfig.HardMaxCacheSize != hardMaxCacheSize { 73 | t.Errorf("bigcacheConfig() HardMaxCacheSize == '%d', want '%d'", bcConfig.HardMaxCacheSize, hardMaxCacheSize) 74 | } 75 | } 76 | 77 | func TestNew(t *testing.T) { 78 | type args struct { 79 | cfg Config 80 | } 81 | 82 | type want struct { 83 | err bool 84 | } 85 | 86 | tests := []struct { 87 | name string 88 | args args 89 | want want 90 | }{ 91 | { 92 | name: "Ok", 93 | args: args{ 94 | cfg: Config{ 95 | FileConfig: config.Cache{ 96 | TTL: 1, 97 | CleanFrequency: 1, 98 | MaxEntries: 1, 99 | MaxEntrySize: 1, 100 | HardMaxCacheSize: 10, 101 | }, 102 | LogLevel: logger.FATAL, 103 | LogOutput: os.Stderr, 104 | }, 105 | }, 106 | want: want{ 107 | err: false, 108 | }, 109 | }, 110 | { 111 | name: "InvalidCleanFrequency", 112 | args: args{ 113 | cfg: Config{ 114 | FileConfig: config.Cache{ 115 | TTL: 1, 116 | CleanFrequency: 0, 117 | MaxEntries: 1, 118 | MaxEntrySize: 1, 119 | HardMaxCacheSize: 10, 120 | }, 121 | LogLevel: logger.FATAL, 122 | LogOutput: os.Stderr, 123 | }, 124 | }, 125 | want: want{ 126 | err: true, 127 | }, 128 | }, 129 | } 130 | for _, tt := range tests { 131 | t.Run(tt.name, func(t *testing.T) { 132 | c, err := New(tt.args.cfg) 133 | if (err != nil) != tt.want.err { 134 | t.Errorf("New() error = '%v', want '%v'", err, tt.want.err) 135 | return 136 | } 137 | 138 | if tt.want.err { 139 | return 140 | } 141 | 142 | if !reflect.DeepEqual(c.fileConfig, tt.args.cfg.FileConfig) { 143 | t.Errorf("New() fileConfig == '%v', want '%v'", c.fileConfig, tt.args.cfg.FileConfig) 144 | } 145 | 146 | if c.bc == nil { 147 | t.Errorf("New() bc is '%v'", nil) 148 | } 149 | }) 150 | } 151 | } 152 | 153 | func TestCache_SetAndGetAndDel(t *testing.T) { 154 | e := getEntryTest() 155 | entry := AcquireEntry() 156 | 157 | k := "www.kratgo.com" 158 | 159 | err := testCache.Set(k, e) 160 | if err != nil { 161 | t.Fatalf("Unexpected error: %v", err) 162 | } 163 | 164 | err = testCache.Get(k, entry) 165 | if err != nil { 166 | t.Fatalf("Unexpected error: %v", err) 167 | } 168 | 169 | if !reflect.DeepEqual(e, *entry) { 170 | t.Errorf("The key '%s' has not been save in cache", k) 171 | } 172 | 173 | entry.Reset() 174 | 175 | err = testCache.Del(k) 176 | if err != nil { 177 | t.Fatalf("Unexpected error: %v", err) 178 | } 179 | 180 | err = testCache.Get(k, entry) 181 | if err != nil { 182 | t.Fatalf("Unexpected error: %v", err) 183 | } 184 | 185 | if reflect.DeepEqual(e, *entry) { 186 | t.Errorf("The key '%s' has not been delete from cache", k) 187 | } 188 | } 189 | 190 | func TestCache_SetAndGetAndDel_Bytes(t *testing.T) { 191 | e := getEntryTest() 192 | entry := AcquireEntry() 193 | 194 | k := []byte("www.kratgo.com") 195 | 196 | err := testCache.SetBytes(k, e) 197 | if err != nil { 198 | t.Fatalf("Unexpected error: %v", err) 199 | } 200 | 201 | err = testCache.GetBytes(k, entry) 202 | if err != nil { 203 | t.Fatalf("Unexpected error: %v", err) 204 | } 205 | 206 | if !reflect.DeepEqual(e, *entry) { 207 | t.Errorf("The key '%s' has not been save in cache", k) 208 | } 209 | 210 | entry.Reset() 211 | 212 | err = testCache.DelBytes(k) 213 | if err != nil { 214 | t.Fatalf("Unexpected error: %v", err) 215 | } 216 | 217 | err = testCache.GetBytes(k, entry) 218 | if err != nil { 219 | t.Fatalf("Unexpected error: %v", err) 220 | } 221 | 222 | if reflect.DeepEqual(e, *entry) { 223 | t.Errorf("The key '%s' has not been delete from cache", k) 224 | } 225 | } 226 | 227 | func TestCache_Iterator(t *testing.T) { 228 | e := getEntryTest() 229 | 230 | k := "www.kratgo.com" 231 | 232 | err := testCache.Set(k, e) 233 | if err != nil { 234 | t.Fatalf("Unexpected error: %v", err) 235 | } 236 | 237 | iter := testCache.Iterator() 238 | if iter == nil { 239 | t.Errorf("Could not get iterator from cache") 240 | } 241 | } 242 | 243 | func TestCache_Len(t *testing.T) { 244 | e := getEntryTest() 245 | 246 | k := "www.kratgo.com" 247 | 248 | err := testCache.Set(k, e) 249 | if err != nil { 250 | t.Fatalf("Unexpected error: %v", err) 251 | } 252 | 253 | wantLength := 1 254 | length := testCache.Len() 255 | if length != wantLength { 256 | t.Errorf("Cache.Len() == '%d', want '%d'", length, wantLength) 257 | } 258 | } 259 | 260 | func TestCache_Reset(t *testing.T) { 261 | e := getEntryTest() 262 | 263 | k := "www.kratgo.com" 264 | 265 | err := testCache.Set(k, e) 266 | if err != nil { 267 | t.Fatalf("Unexpected error: %v", err) 268 | } 269 | 270 | testCache.Reset() 271 | 272 | wantLength := 0 273 | length := testCache.Len() 274 | if length != wantLength { 275 | t.Errorf("Cache.Len() == '%d', want '%d'", length, wantLength) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /modules/cache/cache_types.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | //go:generate msgp 4 | 5 | // ResponseHeader ... 6 | type ResponseHeader struct { 7 | Key []byte 8 | Value []byte 9 | } 10 | 11 | // Response ... 12 | type Response struct { 13 | Path []byte 14 | Body []byte 15 | Headers []ResponseHeader 16 | } 17 | 18 | //Entry ... 19 | type Entry struct { 20 | Responses []Response 21 | } 22 | -------------------------------------------------------------------------------- /modules/cache/cache_types_gen.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // Code generated by github.com/tinylib/msgp DO NOT EDIT. 4 | 5 | import ( 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | // DecodeMsg implements msgp.Decodable 10 | func (z *Entry) DecodeMsg(dc *msgp.Reader) (err error) { 11 | var field []byte 12 | _ = field 13 | var zb0001 uint32 14 | zb0001, err = dc.ReadMapHeader() 15 | if err != nil { 16 | err = msgp.WrapError(err) 17 | return 18 | } 19 | for zb0001 > 0 { 20 | zb0001-- 21 | field, err = dc.ReadMapKeyPtr() 22 | if err != nil { 23 | err = msgp.WrapError(err) 24 | return 25 | } 26 | switch msgp.UnsafeString(field) { 27 | case "Responses": 28 | var zb0002 uint32 29 | zb0002, err = dc.ReadArrayHeader() 30 | if err != nil { 31 | err = msgp.WrapError(err, "Responses") 32 | return 33 | } 34 | if cap(z.Responses) >= int(zb0002) { 35 | z.Responses = (z.Responses)[:zb0002] 36 | } else { 37 | z.Responses = make([]Response, zb0002) 38 | } 39 | for za0001 := range z.Responses { 40 | err = z.Responses[za0001].DecodeMsg(dc) 41 | if err != nil { 42 | err = msgp.WrapError(err, "Responses", za0001) 43 | return 44 | } 45 | } 46 | default: 47 | err = dc.Skip() 48 | if err != nil { 49 | err = msgp.WrapError(err) 50 | return 51 | } 52 | } 53 | } 54 | return 55 | } 56 | 57 | // EncodeMsg implements msgp.Encodable 58 | func (z *Entry) EncodeMsg(en *msgp.Writer) (err error) { 59 | // map header, size 1 60 | // write "Responses" 61 | err = en.Append(0x81, 0xa9, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73) 62 | if err != nil { 63 | return 64 | } 65 | err = en.WriteArrayHeader(uint32(len(z.Responses))) 66 | if err != nil { 67 | err = msgp.WrapError(err, "Responses") 68 | return 69 | } 70 | for za0001 := range z.Responses { 71 | err = z.Responses[za0001].EncodeMsg(en) 72 | if err != nil { 73 | err = msgp.WrapError(err, "Responses", za0001) 74 | return 75 | } 76 | } 77 | return 78 | } 79 | 80 | // MarshalMsg implements msgp.Marshaler 81 | func (z *Entry) MarshalMsg(b []byte) (o []byte, err error) { 82 | o = msgp.Require(b, z.Msgsize()) 83 | // map header, size 1 84 | // string "Responses" 85 | o = append(o, 0x81, 0xa9, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73) 86 | o = msgp.AppendArrayHeader(o, uint32(len(z.Responses))) 87 | for za0001 := range z.Responses { 88 | o, err = z.Responses[za0001].MarshalMsg(o) 89 | if err != nil { 90 | err = msgp.WrapError(err, "Responses", za0001) 91 | return 92 | } 93 | } 94 | return 95 | } 96 | 97 | // UnmarshalMsg implements msgp.Unmarshaler 98 | func (z *Entry) UnmarshalMsg(bts []byte) (o []byte, err error) { 99 | var field []byte 100 | _ = field 101 | var zb0001 uint32 102 | zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) 103 | if err != nil { 104 | err = msgp.WrapError(err) 105 | return 106 | } 107 | for zb0001 > 0 { 108 | zb0001-- 109 | field, bts, err = msgp.ReadMapKeyZC(bts) 110 | if err != nil { 111 | err = msgp.WrapError(err) 112 | return 113 | } 114 | switch msgp.UnsafeString(field) { 115 | case "Responses": 116 | var zb0002 uint32 117 | zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) 118 | if err != nil { 119 | err = msgp.WrapError(err, "Responses") 120 | return 121 | } 122 | if cap(z.Responses) >= int(zb0002) { 123 | z.Responses = (z.Responses)[:zb0002] 124 | } else { 125 | z.Responses = make([]Response, zb0002) 126 | } 127 | for za0001 := range z.Responses { 128 | bts, err = z.Responses[za0001].UnmarshalMsg(bts) 129 | if err != nil { 130 | err = msgp.WrapError(err, "Responses", za0001) 131 | return 132 | } 133 | } 134 | default: 135 | bts, err = msgp.Skip(bts) 136 | if err != nil { 137 | err = msgp.WrapError(err) 138 | return 139 | } 140 | } 141 | } 142 | o = bts 143 | return 144 | } 145 | 146 | // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message 147 | func (z *Entry) Msgsize() (s int) { 148 | s = 1 + 10 + msgp.ArrayHeaderSize 149 | for za0001 := range z.Responses { 150 | s += z.Responses[za0001].Msgsize() 151 | } 152 | return 153 | } 154 | 155 | // DecodeMsg implements msgp.Decodable 156 | func (z *Response) DecodeMsg(dc *msgp.Reader) (err error) { 157 | var field []byte 158 | _ = field 159 | var zb0001 uint32 160 | zb0001, err = dc.ReadMapHeader() 161 | if err != nil { 162 | err = msgp.WrapError(err) 163 | return 164 | } 165 | for zb0001 > 0 { 166 | zb0001-- 167 | field, err = dc.ReadMapKeyPtr() 168 | if err != nil { 169 | err = msgp.WrapError(err) 170 | return 171 | } 172 | switch msgp.UnsafeString(field) { 173 | case "Path": 174 | z.Path, err = dc.ReadBytes(z.Path) 175 | if err != nil { 176 | err = msgp.WrapError(err, "Path") 177 | return 178 | } 179 | case "Body": 180 | z.Body, err = dc.ReadBytes(z.Body) 181 | if err != nil { 182 | err = msgp.WrapError(err, "Body") 183 | return 184 | } 185 | case "Headers": 186 | var zb0002 uint32 187 | zb0002, err = dc.ReadArrayHeader() 188 | if err != nil { 189 | err = msgp.WrapError(err, "Headers") 190 | return 191 | } 192 | if cap(z.Headers) >= int(zb0002) { 193 | z.Headers = (z.Headers)[:zb0002] 194 | } else { 195 | z.Headers = make([]ResponseHeader, zb0002) 196 | } 197 | for za0001 := range z.Headers { 198 | var zb0003 uint32 199 | zb0003, err = dc.ReadMapHeader() 200 | if err != nil { 201 | err = msgp.WrapError(err, "Headers", za0001) 202 | return 203 | } 204 | for zb0003 > 0 { 205 | zb0003-- 206 | field, err = dc.ReadMapKeyPtr() 207 | if err != nil { 208 | err = msgp.WrapError(err, "Headers", za0001) 209 | return 210 | } 211 | switch msgp.UnsafeString(field) { 212 | case "Key": 213 | z.Headers[za0001].Key, err = dc.ReadBytes(z.Headers[za0001].Key) 214 | if err != nil { 215 | err = msgp.WrapError(err, "Headers", za0001, "Key") 216 | return 217 | } 218 | case "Value": 219 | z.Headers[za0001].Value, err = dc.ReadBytes(z.Headers[za0001].Value) 220 | if err != nil { 221 | err = msgp.WrapError(err, "Headers", za0001, "Value") 222 | return 223 | } 224 | default: 225 | err = dc.Skip() 226 | if err != nil { 227 | err = msgp.WrapError(err, "Headers", za0001) 228 | return 229 | } 230 | } 231 | } 232 | } 233 | default: 234 | err = dc.Skip() 235 | if err != nil { 236 | err = msgp.WrapError(err) 237 | return 238 | } 239 | } 240 | } 241 | return 242 | } 243 | 244 | // EncodeMsg implements msgp.Encodable 245 | func (z *Response) EncodeMsg(en *msgp.Writer) (err error) { 246 | // map header, size 3 247 | // write "Path" 248 | err = en.Append(0x83, 0xa4, 0x50, 0x61, 0x74, 0x68) 249 | if err != nil { 250 | return 251 | } 252 | err = en.WriteBytes(z.Path) 253 | if err != nil { 254 | err = msgp.WrapError(err, "Path") 255 | return 256 | } 257 | // write "Body" 258 | err = en.Append(0xa4, 0x42, 0x6f, 0x64, 0x79) 259 | if err != nil { 260 | return 261 | } 262 | err = en.WriteBytes(z.Body) 263 | if err != nil { 264 | err = msgp.WrapError(err, "Body") 265 | return 266 | } 267 | // write "Headers" 268 | err = en.Append(0xa7, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) 269 | if err != nil { 270 | return 271 | } 272 | err = en.WriteArrayHeader(uint32(len(z.Headers))) 273 | if err != nil { 274 | err = msgp.WrapError(err, "Headers") 275 | return 276 | } 277 | for za0001 := range z.Headers { 278 | // map header, size 2 279 | // write "Key" 280 | err = en.Append(0x82, 0xa3, 0x4b, 0x65, 0x79) 281 | if err != nil { 282 | return 283 | } 284 | err = en.WriteBytes(z.Headers[za0001].Key) 285 | if err != nil { 286 | err = msgp.WrapError(err, "Headers", za0001, "Key") 287 | return 288 | } 289 | // write "Value" 290 | err = en.Append(0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65) 291 | if err != nil { 292 | return 293 | } 294 | err = en.WriteBytes(z.Headers[za0001].Value) 295 | if err != nil { 296 | err = msgp.WrapError(err, "Headers", za0001, "Value") 297 | return 298 | } 299 | } 300 | return 301 | } 302 | 303 | // MarshalMsg implements msgp.Marshaler 304 | func (z *Response) MarshalMsg(b []byte) (o []byte, err error) { 305 | o = msgp.Require(b, z.Msgsize()) 306 | // map header, size 3 307 | // string "Path" 308 | o = append(o, 0x83, 0xa4, 0x50, 0x61, 0x74, 0x68) 309 | o = msgp.AppendBytes(o, z.Path) 310 | // string "Body" 311 | o = append(o, 0xa4, 0x42, 0x6f, 0x64, 0x79) 312 | o = msgp.AppendBytes(o, z.Body) 313 | // string "Headers" 314 | o = append(o, 0xa7, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) 315 | o = msgp.AppendArrayHeader(o, uint32(len(z.Headers))) 316 | for za0001 := range z.Headers { 317 | // map header, size 2 318 | // string "Key" 319 | o = append(o, 0x82, 0xa3, 0x4b, 0x65, 0x79) 320 | o = msgp.AppendBytes(o, z.Headers[za0001].Key) 321 | // string "Value" 322 | o = append(o, 0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65) 323 | o = msgp.AppendBytes(o, z.Headers[za0001].Value) 324 | } 325 | return 326 | } 327 | 328 | // UnmarshalMsg implements msgp.Unmarshaler 329 | func (z *Response) UnmarshalMsg(bts []byte) (o []byte, err error) { 330 | var field []byte 331 | _ = field 332 | var zb0001 uint32 333 | zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) 334 | if err != nil { 335 | err = msgp.WrapError(err) 336 | return 337 | } 338 | for zb0001 > 0 { 339 | zb0001-- 340 | field, bts, err = msgp.ReadMapKeyZC(bts) 341 | if err != nil { 342 | err = msgp.WrapError(err) 343 | return 344 | } 345 | switch msgp.UnsafeString(field) { 346 | case "Path": 347 | z.Path, bts, err = msgp.ReadBytesBytes(bts, z.Path) 348 | if err != nil { 349 | err = msgp.WrapError(err, "Path") 350 | return 351 | } 352 | case "Body": 353 | z.Body, bts, err = msgp.ReadBytesBytes(bts, z.Body) 354 | if err != nil { 355 | err = msgp.WrapError(err, "Body") 356 | return 357 | } 358 | case "Headers": 359 | var zb0002 uint32 360 | zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) 361 | if err != nil { 362 | err = msgp.WrapError(err, "Headers") 363 | return 364 | } 365 | if cap(z.Headers) >= int(zb0002) { 366 | z.Headers = (z.Headers)[:zb0002] 367 | } else { 368 | z.Headers = make([]ResponseHeader, zb0002) 369 | } 370 | for za0001 := range z.Headers { 371 | var zb0003 uint32 372 | zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) 373 | if err != nil { 374 | err = msgp.WrapError(err, "Headers", za0001) 375 | return 376 | } 377 | for zb0003 > 0 { 378 | zb0003-- 379 | field, bts, err = msgp.ReadMapKeyZC(bts) 380 | if err != nil { 381 | err = msgp.WrapError(err, "Headers", za0001) 382 | return 383 | } 384 | switch msgp.UnsafeString(field) { 385 | case "Key": 386 | z.Headers[za0001].Key, bts, err = msgp.ReadBytesBytes(bts, z.Headers[za0001].Key) 387 | if err != nil { 388 | err = msgp.WrapError(err, "Headers", za0001, "Key") 389 | return 390 | } 391 | case "Value": 392 | z.Headers[za0001].Value, bts, err = msgp.ReadBytesBytes(bts, z.Headers[za0001].Value) 393 | if err != nil { 394 | err = msgp.WrapError(err, "Headers", za0001, "Value") 395 | return 396 | } 397 | default: 398 | bts, err = msgp.Skip(bts) 399 | if err != nil { 400 | err = msgp.WrapError(err, "Headers", za0001) 401 | return 402 | } 403 | } 404 | } 405 | } 406 | default: 407 | bts, err = msgp.Skip(bts) 408 | if err != nil { 409 | err = msgp.WrapError(err) 410 | return 411 | } 412 | } 413 | } 414 | o = bts 415 | return 416 | } 417 | 418 | // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message 419 | func (z *Response) Msgsize() (s int) { 420 | s = 1 + 5 + msgp.BytesPrefixSize + len(z.Path) + 5 + msgp.BytesPrefixSize + len(z.Body) + 8 + msgp.ArrayHeaderSize 421 | for za0001 := range z.Headers { 422 | s += 1 + 4 + msgp.BytesPrefixSize + len(z.Headers[za0001].Key) + 6 + msgp.BytesPrefixSize + len(z.Headers[za0001].Value) 423 | } 424 | return 425 | } 426 | 427 | // DecodeMsg implements msgp.Decodable 428 | func (z *ResponseHeader) DecodeMsg(dc *msgp.Reader) (err error) { 429 | var field []byte 430 | _ = field 431 | var zb0001 uint32 432 | zb0001, err = dc.ReadMapHeader() 433 | if err != nil { 434 | err = msgp.WrapError(err) 435 | return 436 | } 437 | for zb0001 > 0 { 438 | zb0001-- 439 | field, err = dc.ReadMapKeyPtr() 440 | if err != nil { 441 | err = msgp.WrapError(err) 442 | return 443 | } 444 | switch msgp.UnsafeString(field) { 445 | case "Key": 446 | z.Key, err = dc.ReadBytes(z.Key) 447 | if err != nil { 448 | err = msgp.WrapError(err, "Key") 449 | return 450 | } 451 | case "Value": 452 | z.Value, err = dc.ReadBytes(z.Value) 453 | if err != nil { 454 | err = msgp.WrapError(err, "Value") 455 | return 456 | } 457 | default: 458 | err = dc.Skip() 459 | if err != nil { 460 | err = msgp.WrapError(err) 461 | return 462 | } 463 | } 464 | } 465 | return 466 | } 467 | 468 | // EncodeMsg implements msgp.Encodable 469 | func (z *ResponseHeader) EncodeMsg(en *msgp.Writer) (err error) { 470 | // map header, size 2 471 | // write "Key" 472 | err = en.Append(0x82, 0xa3, 0x4b, 0x65, 0x79) 473 | if err != nil { 474 | return 475 | } 476 | err = en.WriteBytes(z.Key) 477 | if err != nil { 478 | err = msgp.WrapError(err, "Key") 479 | return 480 | } 481 | // write "Value" 482 | err = en.Append(0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65) 483 | if err != nil { 484 | return 485 | } 486 | err = en.WriteBytes(z.Value) 487 | if err != nil { 488 | err = msgp.WrapError(err, "Value") 489 | return 490 | } 491 | return 492 | } 493 | 494 | // MarshalMsg implements msgp.Marshaler 495 | func (z *ResponseHeader) MarshalMsg(b []byte) (o []byte, err error) { 496 | o = msgp.Require(b, z.Msgsize()) 497 | // map header, size 2 498 | // string "Key" 499 | o = append(o, 0x82, 0xa3, 0x4b, 0x65, 0x79) 500 | o = msgp.AppendBytes(o, z.Key) 501 | // string "Value" 502 | o = append(o, 0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65) 503 | o = msgp.AppendBytes(o, z.Value) 504 | return 505 | } 506 | 507 | // UnmarshalMsg implements msgp.Unmarshaler 508 | func (z *ResponseHeader) UnmarshalMsg(bts []byte) (o []byte, err error) { 509 | var field []byte 510 | _ = field 511 | var zb0001 uint32 512 | zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) 513 | if err != nil { 514 | err = msgp.WrapError(err) 515 | return 516 | } 517 | for zb0001 > 0 { 518 | zb0001-- 519 | field, bts, err = msgp.ReadMapKeyZC(bts) 520 | if err != nil { 521 | err = msgp.WrapError(err) 522 | return 523 | } 524 | switch msgp.UnsafeString(field) { 525 | case "Key": 526 | z.Key, bts, err = msgp.ReadBytesBytes(bts, z.Key) 527 | if err != nil { 528 | err = msgp.WrapError(err, "Key") 529 | return 530 | } 531 | case "Value": 532 | z.Value, bts, err = msgp.ReadBytesBytes(bts, z.Value) 533 | if err != nil { 534 | err = msgp.WrapError(err, "Value") 535 | return 536 | } 537 | default: 538 | bts, err = msgp.Skip(bts) 539 | if err != nil { 540 | err = msgp.WrapError(err) 541 | return 542 | } 543 | } 544 | } 545 | o = bts 546 | return 547 | } 548 | 549 | // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message 550 | func (z *ResponseHeader) Msgsize() (s int) { 551 | s = 1 + 4 + msgp.BytesPrefixSize + len(z.Key) + 6 + msgp.BytesPrefixSize + len(z.Value) 552 | return 553 | } 554 | -------------------------------------------------------------------------------- /modules/cache/cache_types_gen_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // Code generated by github.com/tinylib/msgp DO NOT EDIT. 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/tinylib/msgp/msgp" 10 | ) 11 | 12 | func TestMarshalUnmarshalEntry(t *testing.T) { 13 | v := Entry{} 14 | bts, err := v.MarshalMsg(nil) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | left, err := v.UnmarshalMsg(bts) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if len(left) > 0 { 23 | t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) 24 | } 25 | 26 | left, err = msgp.Skip(bts) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if len(left) > 0 { 31 | t.Errorf("%d bytes left over after Skip(): %q", len(left), left) 32 | } 33 | } 34 | 35 | func BenchmarkMarshalMsgEntry(b *testing.B) { 36 | v := Entry{} 37 | b.ReportAllocs() 38 | b.ResetTimer() 39 | for i := 0; i < b.N; i++ { 40 | v.MarshalMsg(nil) 41 | } 42 | } 43 | 44 | func BenchmarkAppendMsgEntry(b *testing.B) { 45 | v := Entry{} 46 | bts := make([]byte, 0, v.Msgsize()) 47 | bts, _ = v.MarshalMsg(bts[0:0]) 48 | b.SetBytes(int64(len(bts))) 49 | b.ReportAllocs() 50 | b.ResetTimer() 51 | for i := 0; i < b.N; i++ { 52 | bts, _ = v.MarshalMsg(bts[0:0]) 53 | } 54 | } 55 | 56 | func BenchmarkUnmarshalEntry(b *testing.B) { 57 | v := Entry{} 58 | bts, _ := v.MarshalMsg(nil) 59 | b.ReportAllocs() 60 | b.SetBytes(int64(len(bts))) 61 | b.ResetTimer() 62 | for i := 0; i < b.N; i++ { 63 | _, err := v.UnmarshalMsg(bts) 64 | if err != nil { 65 | b.Fatal(err) 66 | } 67 | } 68 | } 69 | 70 | func TestEncodeDecodeEntry(t *testing.T) { 71 | v := Entry{} 72 | var buf bytes.Buffer 73 | msgp.Encode(&buf, &v) 74 | 75 | m := v.Msgsize() 76 | if buf.Len() > m { 77 | t.Log("WARNING: TestEncodeDecodeEntry Msgsize() is inaccurate") 78 | } 79 | 80 | vn := Entry{} 81 | err := msgp.Decode(&buf, &vn) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | 86 | buf.Reset() 87 | msgp.Encode(&buf, &v) 88 | err = msgp.NewReader(&buf).Skip() 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | } 93 | 94 | func BenchmarkEncodeEntry(b *testing.B) { 95 | v := Entry{} 96 | var buf bytes.Buffer 97 | msgp.Encode(&buf, &v) 98 | b.SetBytes(int64(buf.Len())) 99 | en := msgp.NewWriter(msgp.Nowhere) 100 | b.ReportAllocs() 101 | b.ResetTimer() 102 | for i := 0; i < b.N; i++ { 103 | v.EncodeMsg(en) 104 | } 105 | en.Flush() 106 | } 107 | 108 | func BenchmarkDecodeEntry(b *testing.B) { 109 | v := Entry{} 110 | var buf bytes.Buffer 111 | msgp.Encode(&buf, &v) 112 | b.SetBytes(int64(buf.Len())) 113 | rd := msgp.NewEndlessReader(buf.Bytes(), b) 114 | dc := msgp.NewReader(rd) 115 | b.ReportAllocs() 116 | b.ResetTimer() 117 | for i := 0; i < b.N; i++ { 118 | err := v.DecodeMsg(dc) 119 | if err != nil { 120 | b.Fatal(err) 121 | } 122 | } 123 | } 124 | 125 | func TestMarshalUnmarshalResponse(t *testing.T) { 126 | v := Response{} 127 | bts, err := v.MarshalMsg(nil) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | left, err := v.UnmarshalMsg(bts) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | if len(left) > 0 { 136 | t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) 137 | } 138 | 139 | left, err = msgp.Skip(bts) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | if len(left) > 0 { 144 | t.Errorf("%d bytes left over after Skip(): %q", len(left), left) 145 | } 146 | } 147 | 148 | func BenchmarkMarshalMsgResponse(b *testing.B) { 149 | v := Response{} 150 | b.ReportAllocs() 151 | b.ResetTimer() 152 | for i := 0; i < b.N; i++ { 153 | v.MarshalMsg(nil) 154 | } 155 | } 156 | 157 | func BenchmarkAppendMsgResponse(b *testing.B) { 158 | v := Response{} 159 | bts := make([]byte, 0, v.Msgsize()) 160 | bts, _ = v.MarshalMsg(bts[0:0]) 161 | b.SetBytes(int64(len(bts))) 162 | b.ReportAllocs() 163 | b.ResetTimer() 164 | for i := 0; i < b.N; i++ { 165 | bts, _ = v.MarshalMsg(bts[0:0]) 166 | } 167 | } 168 | 169 | func BenchmarkUnmarshalResponse(b *testing.B) { 170 | v := Response{} 171 | bts, _ := v.MarshalMsg(nil) 172 | b.ReportAllocs() 173 | b.SetBytes(int64(len(bts))) 174 | b.ResetTimer() 175 | for i := 0; i < b.N; i++ { 176 | _, err := v.UnmarshalMsg(bts) 177 | if err != nil { 178 | b.Fatal(err) 179 | } 180 | } 181 | } 182 | 183 | func TestEncodeDecodeResponse(t *testing.T) { 184 | v := Response{} 185 | var buf bytes.Buffer 186 | msgp.Encode(&buf, &v) 187 | 188 | m := v.Msgsize() 189 | if buf.Len() > m { 190 | t.Log("WARNING: TestEncodeDecodeResponse Msgsize() is inaccurate") 191 | } 192 | 193 | vn := Response{} 194 | err := msgp.Decode(&buf, &vn) 195 | if err != nil { 196 | t.Error(err) 197 | } 198 | 199 | buf.Reset() 200 | msgp.Encode(&buf, &v) 201 | err = msgp.NewReader(&buf).Skip() 202 | if err != nil { 203 | t.Error(err) 204 | } 205 | } 206 | 207 | func BenchmarkEncodeResponse(b *testing.B) { 208 | v := Response{} 209 | var buf bytes.Buffer 210 | msgp.Encode(&buf, &v) 211 | b.SetBytes(int64(buf.Len())) 212 | en := msgp.NewWriter(msgp.Nowhere) 213 | b.ReportAllocs() 214 | b.ResetTimer() 215 | for i := 0; i < b.N; i++ { 216 | v.EncodeMsg(en) 217 | } 218 | en.Flush() 219 | } 220 | 221 | func BenchmarkDecodeResponse(b *testing.B) { 222 | v := Response{} 223 | var buf bytes.Buffer 224 | msgp.Encode(&buf, &v) 225 | b.SetBytes(int64(buf.Len())) 226 | rd := msgp.NewEndlessReader(buf.Bytes(), b) 227 | dc := msgp.NewReader(rd) 228 | b.ReportAllocs() 229 | b.ResetTimer() 230 | for i := 0; i < b.N; i++ { 231 | err := v.DecodeMsg(dc) 232 | if err != nil { 233 | b.Fatal(err) 234 | } 235 | } 236 | } 237 | 238 | func TestMarshalUnmarshalResponseHeader(t *testing.T) { 239 | v := ResponseHeader{} 240 | bts, err := v.MarshalMsg(nil) 241 | if err != nil { 242 | t.Fatal(err) 243 | } 244 | left, err := v.UnmarshalMsg(bts) 245 | if err != nil { 246 | t.Fatal(err) 247 | } 248 | if len(left) > 0 { 249 | t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) 250 | } 251 | 252 | left, err = msgp.Skip(bts) 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | if len(left) > 0 { 257 | t.Errorf("%d bytes left over after Skip(): %q", len(left), left) 258 | } 259 | } 260 | 261 | func BenchmarkMarshalMsgResponseHeader(b *testing.B) { 262 | v := ResponseHeader{} 263 | b.ReportAllocs() 264 | b.ResetTimer() 265 | for i := 0; i < b.N; i++ { 266 | v.MarshalMsg(nil) 267 | } 268 | } 269 | 270 | func BenchmarkAppendMsgResponseHeader(b *testing.B) { 271 | v := ResponseHeader{} 272 | bts := make([]byte, 0, v.Msgsize()) 273 | bts, _ = v.MarshalMsg(bts[0:0]) 274 | b.SetBytes(int64(len(bts))) 275 | b.ReportAllocs() 276 | b.ResetTimer() 277 | for i := 0; i < b.N; i++ { 278 | bts, _ = v.MarshalMsg(bts[0:0]) 279 | } 280 | } 281 | 282 | func BenchmarkUnmarshalResponseHeader(b *testing.B) { 283 | v := ResponseHeader{} 284 | bts, _ := v.MarshalMsg(nil) 285 | b.ReportAllocs() 286 | b.SetBytes(int64(len(bts))) 287 | b.ResetTimer() 288 | for i := 0; i < b.N; i++ { 289 | _, err := v.UnmarshalMsg(bts) 290 | if err != nil { 291 | b.Fatal(err) 292 | } 293 | } 294 | } 295 | 296 | func TestEncodeDecodeResponseHeader(t *testing.T) { 297 | v := ResponseHeader{} 298 | var buf bytes.Buffer 299 | msgp.Encode(&buf, &v) 300 | 301 | m := v.Msgsize() 302 | if buf.Len() > m { 303 | t.Log("WARNING: TestEncodeDecodeResponseHeader Msgsize() is inaccurate") 304 | } 305 | 306 | vn := ResponseHeader{} 307 | err := msgp.Decode(&buf, &vn) 308 | if err != nil { 309 | t.Error(err) 310 | } 311 | 312 | buf.Reset() 313 | msgp.Encode(&buf, &v) 314 | err = msgp.NewReader(&buf).Skip() 315 | if err != nil { 316 | t.Error(err) 317 | } 318 | } 319 | 320 | func BenchmarkEncodeResponseHeader(b *testing.B) { 321 | v := ResponseHeader{} 322 | var buf bytes.Buffer 323 | msgp.Encode(&buf, &v) 324 | b.SetBytes(int64(buf.Len())) 325 | en := msgp.NewWriter(msgp.Nowhere) 326 | b.ReportAllocs() 327 | b.ResetTimer() 328 | for i := 0; i < b.N; i++ { 329 | v.EncodeMsg(en) 330 | } 331 | en.Flush() 332 | } 333 | 334 | func BenchmarkDecodeResponseHeader(b *testing.B) { 335 | v := ResponseHeader{} 336 | var buf bytes.Buffer 337 | msgp.Encode(&buf, &v) 338 | b.SetBytes(int64(buf.Len())) 339 | rd := msgp.NewEndlessReader(buf.Bytes(), b) 340 | dc := msgp.NewReader(rd) 341 | b.ReportAllocs() 342 | b.ResetTimer() 343 | for i := 0; i < b.N; i++ { 344 | err := v.DecodeMsg(dc) 345 | if err != nil { 346 | b.Fatal(err) 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /modules/cache/const.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | const defaultBigcacheShards = 1024 // power of two 4 | -------------------------------------------------------------------------------- /modules/cache/entry.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var entryPool = sync.Pool{ 10 | New: func() interface{} { 11 | return new(Entry) 12 | }, 13 | } 14 | 15 | // AcquireEntry ... 16 | func AcquireEntry() *Entry { 17 | return entryPool.Get().(*Entry) 18 | } 19 | 20 | // ReleaseEntry ... 21 | func ReleaseEntry(e *Entry) { 22 | e.Reset() 23 | entryPool.Put(e) 24 | } 25 | 26 | // Reset ... 27 | func (e *Entry) Reset() { 28 | e.Responses = e.Responses[:0] 29 | } 30 | 31 | func (e *Entry) swap(data []Response, i, j int) []Response { 32 | data[i], data[j] = data[j], data[i] 33 | 34 | return data 35 | } 36 | 37 | func (e *Entry) allocResponse(data []Response) ([]Response, *Response) { 38 | n := len(data) 39 | 40 | if cap(data) > n { 41 | data = data[:n+1] 42 | } else { 43 | data = append(data, Response{}) 44 | } 45 | 46 | return data, &data[n] 47 | } 48 | 49 | func (e *Entry) appendResponse(data []Response, resp Response) []Response { 50 | data, r := e.allocResponse(data) 51 | 52 | r.Path = append(r.Path[:0], resp.Path...) 53 | r.Body = append(r.Body[:0], resp.Body...) 54 | r.Headers = resp.Headers 55 | 56 | return data 57 | } 58 | 59 | // HasResponse ... 60 | func (e Entry) HasResponse(path []byte) bool { 61 | for i, n := 0, len(e.Responses); i < n; i++ { 62 | resp := &e.Responses[i] 63 | if bytes.Equal(path, resp.Path) { 64 | return true 65 | } 66 | } 67 | 68 | return false 69 | } 70 | 71 | // GetAllResponses ... 72 | func (e Entry) GetAllResponses() []Response { 73 | return e.Responses 74 | } 75 | 76 | // Len ... 77 | func (e Entry) Len() int { 78 | return len(e.Responses) 79 | } 80 | 81 | // GetResponse ... 82 | func (e Entry) GetResponse(path []byte) *Response { 83 | n := len(e.Responses) 84 | for i := 0; i < n; i++ { 85 | resp := &e.Responses[i] 86 | if bytes.Equal(path, resp.Path) { 87 | return resp 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // SetResponse ... 95 | func (e *Entry) SetResponse(resp Response) { 96 | r := e.GetResponse(resp.Path) 97 | if r != nil { 98 | r.Body = append(r.Body[:0], resp.Body...) 99 | r.Headers = resp.Headers 100 | 101 | return 102 | } 103 | 104 | e.Responses = e.appendResponse(e.Responses, resp) 105 | } 106 | 107 | // DelResponse ... 108 | func (e *Entry) DelResponse(path []byte) { 109 | responses := e.GetAllResponses() 110 | 111 | for i, n := 0, len(responses); i < n; i++ { 112 | resp := &responses[i] 113 | if bytes.Equal(path, resp.Path) { 114 | n-- 115 | if i != n { 116 | e.swap(responses, i, n) 117 | i-- 118 | } 119 | responses = responses[:n] // Remove last position 120 | } 121 | } 122 | 123 | e.Responses = responses 124 | } 125 | 126 | // Marshal ... 127 | func Marshal(src Entry) ([]byte, error) { 128 | b, _ := src.MarshalMsg(nil) 129 | 130 | return b, nil 131 | } 132 | 133 | // Unmarshal ... 134 | func Unmarshal(dst *Entry, data []byte) error { 135 | if len(data) == 0 { 136 | return nil 137 | } 138 | 139 | if _, err := dst.UnmarshalMsg(data); err != nil { 140 | return fmt.Errorf("Could not unmarshal '%s': %v", data, err) 141 | } 142 | 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /modules/cache/entry_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func getEntryTest() Entry { 10 | r1 := Response{ 11 | Path: []byte("/cache/"), 12 | Body: []byte("Response body"), 13 | Headers: []ResponseHeader{ 14 | { 15 | Key: []byte("Key1"), 16 | Value: []byte("Value1"), 17 | }, 18 | }, 19 | } 20 | r2 := Response{ 21 | Path: []byte("/cache/2/"), 22 | Body: []byte("Response body 2"), 23 | Headers: []ResponseHeader{ 24 | { 25 | Key: []byte("Key1"), 26 | Value: []byte("Value1"), 27 | }, 28 | }, 29 | } 30 | 31 | return Entry{ 32 | Responses: []Response{ 33 | r1, 34 | r2, 35 | }, 36 | } 37 | } 38 | 39 | func TestAcquireEntry(t *testing.T) { 40 | e := AcquireEntry() 41 | if e == nil { 42 | t.Errorf("AcquireEntry() returns '%v'", nil) 43 | } 44 | } 45 | 46 | func TestReleaseEntry(t *testing.T) { 47 | e := AcquireEntry() 48 | r := AcquireResponse() 49 | 50 | e.SetResponse(*r) 51 | 52 | ReleaseEntry(e) 53 | 54 | if e.Len() > 0 { 55 | t.Errorf("ReleaseEntry() entry has not been reset") 56 | } 57 | } 58 | 59 | func TestEntry_Reset(t *testing.T) { 60 | e := getEntryTest() 61 | e.Reset() 62 | 63 | if len(e.Responses) > 0 { 64 | t.Errorf("Entry.Reset() has not been reset") 65 | } 66 | } 67 | 68 | func TestEntry_swap(t *testing.T) { 69 | e := getEntryTest() 70 | r1 := e.Responses[0] 71 | r2 := e.Responses[1] 72 | 73 | e.swap(e.Responses, 0, 1) 74 | 75 | if reflect.DeepEqual(e.Responses[0], r1) { 76 | t.Errorf("Entry.swap() == '%v', want '%v'", e.Responses[0], r2) 77 | } 78 | 79 | if reflect.DeepEqual(e.Responses[1], r2) { 80 | t.Errorf("Entry.swap() == '%v', want '%v'", e.Responses[1], r1) 81 | } 82 | } 83 | 84 | func TestEntry_allocResponse(t *testing.T) { 85 | e := getEntryTest() 86 | 87 | wantCapacity := cap(e.Responses) * 2 // Capacity is incremented by power of two 88 | 89 | var r *Response 90 | e.Responses, r = e.allocResponse(e.Responses) 91 | 92 | if r == nil { 93 | t.Errorf("Entry.allocResponse() not returns a new response pointer") 94 | } 95 | 96 | if cap(e.Responses) != wantCapacity { 97 | t.Errorf("Entry.allocResponse() responses capacity == '%d', want '%d'", cap(e.Responses), wantCapacity) 98 | } 99 | 100 | e.Responses = e.Responses[:len(e.Responses)-1] 101 | e.Responses, _ = e.allocResponse(e.Responses) 102 | 103 | if cap(e.Responses) != wantCapacity { 104 | t.Errorf("Entry.allocResponse() responses capacity == '%d', want '%d'", cap(e.Responses), wantCapacity) 105 | } 106 | } 107 | 108 | func TestEntry_appendResponse(t *testing.T) { 109 | e := getEntryTest() 110 | 111 | length := len(e.Responses) 112 | 113 | r := AcquireResponse() 114 | 115 | e.Responses = e.appendResponse(e.Responses, *r) 116 | 117 | if len(e.Responses) != length+1 { 118 | t.Errorf("Entry.appendResponse() responses.len == '%d', want '%d'", len(e.Responses), length+1) 119 | } 120 | } 121 | 122 | func TestEntry_HasResponse(t *testing.T) { 123 | e := getEntryTest() 124 | r1 := e.Responses[0] 125 | 126 | fakePath := []byte("/fake") 127 | 128 | if !e.HasResponse(r1.Path) { 129 | t.Errorf("Entry.HasResponse() path '%s' == '%v', want '%v'", r1.Path, false, true) 130 | } 131 | 132 | if e.HasResponse(fakePath) { 133 | t.Errorf("Entry.HasResponse() path '%s' == '%v', want '%v'", fakePath, true, false) 134 | } 135 | } 136 | 137 | func TestEntry_GetAllResponses(t *testing.T) { 138 | e := getEntryTest() 139 | 140 | all := e.GetAllResponses() 141 | 142 | if !reflect.DeepEqual(all, e.Responses) { 143 | t.Errorf("Entry.GetAllResponses() == '%v', want '%v'", all, e.Responses) 144 | } 145 | } 146 | 147 | func TestEntry_Len(t *testing.T) { 148 | e := getEntryTest() 149 | 150 | length := len(e.GetAllResponses()) 151 | 152 | if e.Len() != length { 153 | t.Errorf("Entry.Len() == '%d', want '%d'", e.Len(), length) 154 | } 155 | } 156 | 157 | func TestEntry_GetResponse(t *testing.T) { 158 | e := getEntryTest() 159 | r1 := e.Responses[0] 160 | 161 | fakePath := []byte("/fake") 162 | 163 | if r := e.GetResponse(r1.Path); !reflect.DeepEqual(*r, r1) { 164 | t.Errorf("Entry.GetResponse() path '%s' == '%v', want '%v'", r1.Path, *r, r1) 165 | } 166 | 167 | if r := e.GetResponse(fakePath); r != nil { 168 | t.Errorf("Entry.GetResponse() path '%s' == '%v', want '%v'", fakePath, *r, nil) 169 | } 170 | } 171 | 172 | func TestEntry_SetResponse(t *testing.T) { 173 | e := getEntryTest() 174 | 175 | wantLength := e.Len() + 1 176 | 177 | r := AcquireResponse() 178 | r.Path = []byte("/kratgo/fast") 179 | r.Body = []byte("Body Kratgo Fast") 180 | 181 | e.SetResponse(*r) 182 | 183 | length := e.Len() 184 | 185 | if !e.HasResponse(r.Path) { 186 | t.Errorf("Entry.SetResponse() has not been set a new response") 187 | } 188 | 189 | // Update respose (same r.Path) 190 | r.Body = []byte("UPDATED Body Kratgo Fast") 191 | r.SetHeader([]byte("key"), []byte("value")) 192 | 193 | e.SetResponse(*r) 194 | 195 | if length != wantLength { 196 | t.Errorf("Entry.SetResponse() has not been update the existing response") 197 | } 198 | } 199 | 200 | func TestEntry_DelResponse(t *testing.T) { 201 | e := getEntryTest() 202 | r1 := e.Responses[0] 203 | 204 | e.DelResponse(r1.Path) 205 | 206 | if e.HasResponse(r1.Path) { 207 | t.Errorf("Entry.DelResponse() has not been delete the response") 208 | } 209 | } 210 | 211 | func TestMarshal(t *testing.T) { 212 | e := getEntryTest() 213 | 214 | expected, err := e.MarshalMsg(nil) 215 | if err != nil { 216 | t.Fatalf("Unexpected error: %v", err) 217 | } 218 | 219 | marshalData, err := Marshal(e) 220 | if err != nil { 221 | t.Fatalf("Unexpected error: %v", err) 222 | } 223 | 224 | if !bytes.Equal(marshalData, expected) { 225 | t.Errorf("Marshal() == '%s', want '%s'", marshalData, expected) 226 | } 227 | } 228 | 229 | func TestUnmarshal(t *testing.T) { 230 | type args struct { 231 | charsToDelete int 232 | } 233 | 234 | type want struct { 235 | empty bool 236 | err bool 237 | } 238 | 239 | e := getEntryTest() 240 | marshalData, _ := Marshal(e) 241 | 242 | tests := []struct { 243 | name string 244 | args args 245 | want want 246 | }{ 247 | { 248 | name: "Ok", 249 | args: args{ 250 | charsToDelete: 0, 251 | }, 252 | want: want{ 253 | empty: false, 254 | err: false, 255 | }, 256 | }, 257 | { 258 | name: "Empty", 259 | args: args{ 260 | charsToDelete: len(marshalData), 261 | }, 262 | want: want{ 263 | empty: true, 264 | err: false, 265 | }, 266 | }, 267 | { 268 | name: "Error", 269 | args: args{ 270 | charsToDelete: 1, 271 | }, 272 | want: want{ 273 | err: true, 274 | }, 275 | }, 276 | } 277 | 278 | for _, tt := range tests { 279 | t.Run(tt.name, func(t *testing.T) { 280 | entry := AcquireEntry() 281 | 282 | err := Unmarshal(entry, marshalData[tt.args.charsToDelete:]) 283 | if (err != nil) != tt.want.err { 284 | t.Fatalf("Unexpected error: %v", err) 285 | } 286 | 287 | if tt.want.err { 288 | return 289 | } 290 | 291 | if tt.want.empty { 292 | if entry.Len() > 0 { 293 | t.Errorf("Unmarshal() entry has not been empty: %v'", *entry) 294 | } 295 | 296 | } else if !reflect.DeepEqual(e, *entry) { 297 | t.Errorf("Unmarshal() == '%v', want '%v'", *entry, e) 298 | } 299 | }) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /modules/cache/response.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var responsePool = sync.Pool{ 9 | New: func() interface{} { 10 | return new(Response) 11 | }, 12 | } 13 | 14 | // AcquireResponse ... 15 | func AcquireResponse() *Response { 16 | return responsePool.Get().(*Response) 17 | } 18 | 19 | // ReleaseResponse ... 20 | func ReleaseResponse(r *Response) { 21 | r.Reset() 22 | responsePool.Put(r) 23 | } 24 | 25 | func (r *Response) allocHeader(data []ResponseHeader) ([]ResponseHeader, *ResponseHeader) { 26 | n := len(data) 27 | 28 | if cap(data) > n { 29 | data = data[:n+1] 30 | } else { 31 | data = append(data, ResponseHeader{}) 32 | } 33 | 34 | return data, &data[n] 35 | } 36 | 37 | func (r *Response) appendHeader(data []ResponseHeader, k, v []byte) []ResponseHeader { 38 | data, h := r.allocHeader(data) 39 | 40 | h.Key = append(h.Key[:0], k...) 41 | h.Value = append(h.Value[:0], v...) 42 | 43 | return data 44 | } 45 | 46 | // HasHeader ... 47 | func (r *Response) HasHeader(k, v []byte) bool { 48 | for i, n := 0, len(r.Headers); i < n; i++ { 49 | h := &r.Headers[i] 50 | if bytes.Equal(h.Key, k) && bytes.Equal(h.Value, v) { 51 | return true 52 | } 53 | } 54 | 55 | return false 56 | } 57 | 58 | // SetHeader ... 59 | func (r *Response) SetHeader(k, v []byte) { 60 | r.Headers = r.appendHeader(r.Headers, k, v) 61 | } 62 | 63 | // Reset reset response 64 | func (r *Response) Reset() { 65 | r.Path = r.Path[:0] 66 | r.Body = r.Body[:0] 67 | r.Headers = r.Headers[:0] 68 | } 69 | -------------------------------------------------------------------------------- /modules/cache/response_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func getResponseTest() Response { 9 | return Response{ 10 | Path: []byte("/cache/"), 11 | Body: []byte("Response body"), 12 | Headers: []ResponseHeader{ 13 | { 14 | Key: []byte("Key1"), 15 | Value: []byte("Value1"), 16 | }, 17 | }, 18 | } 19 | } 20 | 21 | func TestAcquireResponse(t *testing.T) { 22 | r := AcquireResponse() 23 | if r == nil { 24 | t.Errorf("AcquireResponse() returns '%v'", nil) 25 | } 26 | } 27 | 28 | func TestReleaseResponse(t *testing.T) { 29 | r := AcquireResponse() 30 | r.Path = []byte("/kratgo") 31 | r.Body = []byte("Kratgo is ultra fast") 32 | r.SetHeader([]byte("key"), []byte("value")) 33 | 34 | ReleaseResponse(r) 35 | 36 | if len(r.Path) > 0 || len(r.Body) > 0 || len(r.Headers) > 0 { 37 | t.Errorf("ReleaseResponse() response has not been reset") 38 | } 39 | } 40 | 41 | func TestResponse_allocHeader(t *testing.T) { 42 | r := AcquireResponse() 43 | r.SetHeader([]byte("key1"), []byte("value1")) 44 | 45 | wantCapacity := cap(r.Headers) * 2 // Capacity is incremented by power of two 46 | 47 | var h *ResponseHeader 48 | r.Headers, h = r.allocHeader(r.Headers) 49 | 50 | if h == nil { 51 | t.Errorf("Response.allocHeader() not returns a new header pointer") 52 | } 53 | 54 | if cap(r.Headers) != wantCapacity { 55 | t.Errorf("Response.allocHeader() headers capacity == '%d', want '%d'", cap(r.Headers), wantCapacity) 56 | } 57 | 58 | r.Headers = r.Headers[:len(r.Headers)-1] 59 | r.Headers, _ = r.allocHeader(r.Headers) 60 | 61 | if cap(r.Headers) != wantCapacity { 62 | t.Errorf("Response.allocHeader() headers capacity == '%d', want '%d'", cap(r.Headers), wantCapacity) 63 | } 64 | } 65 | 66 | func TestResponse_appendHeader(t *testing.T) { 67 | r := getResponseTest() 68 | 69 | length := len(r.Headers) 70 | 71 | r.Headers = r.appendHeader(r.Headers, []byte("key"), []byte("value")) 72 | 73 | if len(r.Headers) != length+1 { 74 | t.Errorf("Entry.appendResponse() responses.len == '%d', want '%d'", len(r.Headers), length+1) 75 | } 76 | } 77 | 78 | func TestResponse_SetHeader(t *testing.T) { 79 | r := getResponseTest() 80 | 81 | k := []byte("newKey") 82 | v := []byte("newValue") 83 | 84 | r.SetHeader(k, v) 85 | 86 | finded := false 87 | for _, h := range r.Headers { 88 | if bytes.Equal(h.Key, k) && bytes.Equal(h.Value, v) { 89 | finded = true 90 | break 91 | } 92 | } 93 | 94 | if !finded { 95 | t.Errorf("The header '%s==%s' has not been set", k, v) 96 | } 97 | } 98 | 99 | func TestResponse_HasHeader(t *testing.T) { 100 | r := getResponseTest() 101 | 102 | k := []byte("newKey") 103 | v := []byte("newValue") 104 | 105 | r.SetHeader(k, v) 106 | 107 | if !r.HasHeader(k, v) { 108 | t.Errorf("The header '%s = %s' not found", k, v) 109 | } 110 | 111 | if r.HasHeader(k, []byte("other value")) { 112 | t.Errorf("The header '%s = %s' found", k, v) 113 | } 114 | } 115 | 116 | func TestResponse_Reset(t *testing.T) { 117 | r := getResponseTest() 118 | 119 | r.Reset() 120 | 121 | if len(r.Path) > 0 { 122 | t.Errorf("Response.Path has not been reset") 123 | } 124 | 125 | if len(r.Body) > 0 { 126 | t.Errorf("Response.Body has not been reset") 127 | } 128 | 129 | if len(r.Headers) > 0 { 130 | t.Errorf("Response.Headers has not been reset") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /modules/cache/types.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/allegro/bigcache/v3" 7 | "github.com/savsgio/go-logger/v4" 8 | "github.com/savsgio/kratgo/modules/config" 9 | ) 10 | 11 | // Config ... 12 | type Config struct { 13 | FileConfig config.Cache 14 | 15 | LogLevel logger.Level 16 | LogOutput io.Writer 17 | } 18 | 19 | // Cache ... 20 | type Cache struct { 21 | fileConfig config.Cache 22 | 23 | bc *bigcache.BigCache 24 | } 25 | -------------------------------------------------------------------------------- /modules/config/const.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const configMethodVar = "$(method)" 4 | const configHostVar = "$(host)" 5 | const configPathVar = "$(path)" 6 | const configContentTypeVar = "$(contentType)" 7 | const configStatusCodeVar = "$(statusCode)" 8 | const configReqHeaderVar = "$(req.header::)" 9 | const configRespHeaderVar = "$(resp.header::)" 10 | const configCookieVar = "$(cookie::)" 11 | 12 | // EvalVarPrefix ... 13 | const EvalVarPrefix = "Krat" 14 | 15 | // EvalMethodVar ... 16 | const EvalMethodVar = EvalVarPrefix + "METHOD" 17 | 18 | // EvalHostVar ... 19 | const EvalHostVar = EvalVarPrefix + "HOST" 20 | 21 | // EvalPathVar ... 22 | const EvalPathVar = EvalVarPrefix + "PATH" 23 | 24 | // EvalContentTypeVar ... 25 | const EvalContentTypeVar = EvalVarPrefix + "CONTENTTYPE" 26 | 27 | // EvalStatusCodeVar ... 28 | const EvalStatusCodeVar = EvalVarPrefix + "STATUSCODE" 29 | 30 | // EvalReqHeaderVar ... 31 | const EvalReqHeaderVar = EvalVarPrefix + "REQHEADER" 32 | 33 | // EvalRespHeaderVar ... 34 | const EvalRespHeaderVar = EvalVarPrefix + "RESPHEADER" 35 | 36 | // EvalCookieVar ... 37 | const EvalCookieVar = EvalVarPrefix + "COOKIE" 38 | -------------------------------------------------------------------------------- /modules/config/parser.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "regexp" 8 | 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | var configEvaluationVars = map[string]string{ 13 | configMethodVar: EvalMethodVar, 14 | configHostVar: EvalHostVar, 15 | configPathVar: EvalPathVar, 16 | configContentTypeVar: EvalContentTypeVar, 17 | configStatusCodeVar: EvalStatusCodeVar, 18 | configReqHeaderVar: EvalReqHeaderVar, 19 | configRespHeaderVar: EvalRespHeaderVar, 20 | configCookieVar: EvalCookieVar, 21 | } 22 | 23 | // ConfigVarRegex ... 24 | var ConfigVarRegex = regexp.MustCompile("\\$\\([a-zA-Z0-9\\-:\\.\\_]+\\)") 25 | 26 | // ConfigReqHeaderVarRegex ... 27 | var ConfigReqHeaderVarRegex = regexp.MustCompile("\\$\\(req\\.header::([a-zA-Z0-9\\-\\_]+)\\)") 28 | 29 | // ConfigRespHeaderVarRegex ... 30 | var ConfigRespHeaderVarRegex = regexp.MustCompile("\\$\\(resp\\.header::([a-zA-Z0-9\\-\\_]+)\\)") 31 | 32 | // ConfigCookieVarRegex ... 33 | var ConfigCookieVarRegex = regexp.MustCompile("\\$\\(cookie::([a-zA-Z0-9\\-\\_]+)\\)") 34 | 35 | // Parse ... 36 | func Parse(path string) (*Config, error) { 37 | data, err := ioutil.ReadFile(path) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | cfg := new(Config) 43 | err = yaml.Unmarshal(data, cfg) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return cfg, nil 49 | } 50 | 51 | // GetEvalParamName ... 52 | func GetEvalParamName(k string) string { 53 | if k == configReqHeaderVar || k == configRespHeaderVar { 54 | return fmt.Sprintf("%s%d", configEvaluationVars[k], rand.Int31n(100)) 55 | 56 | } else if k == configCookieVar { 57 | return fmt.Sprintf("%s%d", configEvaluationVars[k], rand.Int31n(100)) 58 | } 59 | 60 | if v, ok := configEvaluationVars[k]; ok { 61 | return v 62 | } 63 | 64 | return k 65 | } 66 | 67 | // ParseConfigKeys ... 68 | func ParseConfigKeys(s string) (string, string, string) { 69 | for k := range configEvaluationVars { 70 | if k == configReqHeaderVar { 71 | data := ConfigReqHeaderVarRegex.FindStringSubmatch(s) 72 | if len(data) > 1 { 73 | return data[0], GetEvalParamName(k), data[1] 74 | } 75 | } else if k == configRespHeaderVar { 76 | data := ConfigRespHeaderVarRegex.FindStringSubmatch(s) 77 | if len(data) > 1 { 78 | return data[0], GetEvalParamName(k), data[1] 79 | } 80 | 81 | } else if k == configCookieVar { 82 | data := ConfigCookieVarRegex.FindStringSubmatch(s) 83 | if len(data) > 1 { 84 | return data[0], GetEvalParamName(k), data[1] 85 | } 86 | 87 | } else { 88 | data := ConfigVarRegex.FindStringSubmatch(s) 89 | if len(data) > 0 && data[0] == k { 90 | return data[0], GetEvalParamName(data[0]), "" 91 | } 92 | } 93 | } 94 | 95 | return "", "", "" 96 | } 97 | -------------------------------------------------------------------------------- /modules/config/parser_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "reflect" 8 | "regexp" 9 | "testing" 10 | ) 11 | 12 | var yamlConfig = []byte(`logLevel: debug 13 | logOutput: console 14 | 15 | cache: 16 | ttl: 10 17 | cleanFrequency: 1 18 | maxEntries: 600000 19 | maxEntrySize: 500 20 | hardMaxCacheSize: 0 21 | 22 | invalidator: 23 | maxWorkers: 5 24 | 25 | proxy: 26 | addr: 0.0.0.0:6081 27 | backendAddrs: 28 | [ 29 | 1.2.3.4:5678, 30 | ] 31 | response: 32 | headers: 33 | set: 34 | - name: X-Theme 35 | value: $(resp.header::Theme) 36 | 37 | unset: 38 | - name: Set-Cookie 39 | if: $(path) !~ '/preview/' && $(path) !~ '/exit_preview/' && $(cookie::is_preview) == 'True' 40 | 41 | nocache: 42 | - $(method) == 'POST' 43 | - $(host) == 'www.kratgo.com' 44 | 45 | admin: 46 | addr: 0.0.0.0:6082 47 | `) 48 | 49 | func TestParse(t *testing.T) { 50 | type args struct { 51 | filePath string 52 | fileContent []byte 53 | filePerms os.FileMode 54 | } 55 | 56 | type want struct { 57 | err bool 58 | } 59 | 60 | tests := []struct { 61 | name string 62 | args args 63 | want want 64 | }{ 65 | { 66 | name: "Ok", 67 | args: args{ 68 | filePath: "/tmp/kratgo_tests.yml", 69 | fileContent: yamlConfig, 70 | filePerms: 0775, 71 | }, 72 | want: want{ 73 | err: false, 74 | }, 75 | }, 76 | { 77 | name: "InvalidFile", 78 | args: args{ 79 | filePath: "", 80 | }, 81 | want: want{ 82 | err: true, 83 | }, 84 | }, 85 | { 86 | name: "InvalidContent", 87 | args: args{ 88 | filePath: "/tmp/kratgo_tests.yml", 89 | fileContent: []byte("aasd\tsxfa\n:$%·$&·&"), 90 | filePerms: 0775, 91 | }, 92 | want: want{ 93 | err: true, 94 | }, 95 | }, 96 | } 97 | 98 | for _, tt := range tests { 99 | t.Run(tt.name, func(t *testing.T) { 100 | if tt.args.filePath != "" { 101 | err := ioutil.WriteFile(tt.args.filePath, tt.args.fileContent, tt.args.filePerms) 102 | if err != nil { 103 | panic(err) 104 | } 105 | } 106 | 107 | cfg, err := Parse(tt.args.filePath) 108 | if (err != nil) != tt.want.err { 109 | t.Fatalf("Parse() error == '%v', want '%v'", err, tt.want.err) 110 | } 111 | 112 | if tt.want.err { 113 | return 114 | } 115 | 116 | logLevel := "debug" 117 | if cfg.LogLevel != logLevel { 118 | t.Fatalf("Parse() LogLevel == '%s', want '%s'", cfg.LogLevel, logLevel) 119 | } 120 | 121 | logOutput := "console" 122 | if cfg.LogOutput != logOutput { 123 | t.Fatalf("Parse() LogOutput == '%s', want '%s'", cfg.LogOutput, logOutput) 124 | } 125 | 126 | cacheTTL := 10 127 | if cfg.Cache.TTL != cacheTTL { 128 | t.Fatalf("Parse() Cache.TTL == '%d', want '%d'", cfg.Cache.TTL, cacheTTL) 129 | } 130 | 131 | cacheCleanFrequency := 1 132 | if cfg.Cache.CleanFrequency != cacheCleanFrequency { 133 | t.Fatalf("Parse() Cache.CleanFrequency == '%d', want '%d'", cfg.Cache.CleanFrequency, cacheCleanFrequency) 134 | } 135 | 136 | cacheMaxEntries := 600000 137 | if cfg.Cache.MaxEntries != cacheMaxEntries { 138 | t.Fatalf("Parse() Cache.MaxEntries == '%d', want '%d'", cfg.Cache.MaxEntries, cacheMaxEntries) 139 | } 140 | 141 | cacheMaxEntrySize := 500 142 | if cfg.Cache.MaxEntrySize != cacheMaxEntrySize { 143 | t.Fatalf("Parse() Cache.MaxEntrySize == '%d', want '%d'", cfg.Cache.MaxEntrySize, cacheMaxEntrySize) 144 | } 145 | 146 | cacheHardMaxCacheSize := 0 147 | if cfg.Cache.HardMaxCacheSize != cacheHardMaxCacheSize { 148 | t.Fatalf("Parse() Cache.HardMaxCacheSize == '%d', want '%d'", cfg.Cache.HardMaxCacheSize, cacheHardMaxCacheSize) 149 | } 150 | 151 | invalidatorMaxWorkers := int32(5) 152 | if cfg.Invalidator.MaxWorkers != invalidatorMaxWorkers { 153 | t.Fatalf("Parse() Invalidator.MaxWorkers == '%d', want '%d'", cfg.Invalidator.MaxWorkers, invalidatorMaxWorkers) 154 | } 155 | 156 | proxyAddr := "0.0.0.0:6081" 157 | if cfg.Proxy.Addr != proxyAddr { 158 | t.Fatalf("Parse() Proxy.Addr == '%s', want '%s'", cfg.Proxy.Addr, proxyAddr) 159 | } 160 | 161 | proxyBackendAddrs := []string{"1.2.3.4:5678"} 162 | if !reflect.DeepEqual(cfg.Proxy.BackendAddrs, proxyBackendAddrs) { 163 | t.Fatalf("Parse() Proxy.BackendAddrs == '%v', want '%v'", cfg.Proxy.BackendAddrs, proxyBackendAddrs) 164 | } 165 | 166 | proxyResponseHeadersSet := []Header{{Name: "X-Theme", Value: "$(resp.header::Theme)"}} 167 | if !reflect.DeepEqual(cfg.Proxy.Response.Headers.Set, proxyResponseHeadersSet) { 168 | t.Fatalf("Parse() Proxy.Response.Headers.Set == '%v', want '%v'", cfg.Proxy.Response.Headers.Set, proxyResponseHeadersSet) 169 | } 170 | 171 | proxyResponseHeadersUnset := []Header{{Name: "Set-Cookie", When: "$(path) !~ '/preview/' && $(path) !~ '/exit_preview/' && $(cookie::is_preview) == 'True'"}} 172 | if !reflect.DeepEqual(cfg.Proxy.Response.Headers.Unset, proxyResponseHeadersUnset) { 173 | t.Fatalf("Parse() Proxy.Response.Headers.Unset == '%v', want '%v'", cfg.Proxy.Response.Headers.Unset, proxyResponseHeadersUnset) 174 | } 175 | 176 | proxyNocache := []string{"$(method) == 'POST'", "$(host) == 'www.kratgo.com'"} 177 | if !reflect.DeepEqual(cfg.Proxy.Nocache, proxyNocache) { 178 | t.Fatalf("Parse() Proxy.Nocache == '%v', want '%v'", cfg.Proxy.Nocache, proxyNocache) 179 | } 180 | 181 | adminAddr := "0.0.0.0:6082" 182 | if cfg.Admin.Addr != adminAddr { 183 | t.Fatalf("Parse() Admin.Addr == '%s', want '%s'", cfg.Admin.Addr, adminAddr) 184 | } 185 | }) 186 | } 187 | 188 | } 189 | 190 | func TestGetEvalParamName(t *testing.T) { 191 | type args struct { 192 | key string 193 | } 194 | 195 | type want struct { 196 | evalKey string 197 | regexEvalKey *regexp.Regexp 198 | } 199 | 200 | tests := []struct { 201 | name string 202 | args args 203 | want want 204 | }{ 205 | { 206 | name: "method", 207 | args: args{ 208 | key: configMethodVar, 209 | }, 210 | want: want{ 211 | evalKey: EvalMethodVar, 212 | }, 213 | }, 214 | { 215 | name: "host", 216 | args: args{ 217 | key: configHostVar, 218 | }, 219 | want: want{ 220 | evalKey: EvalHostVar, 221 | }, 222 | }, 223 | { 224 | name: "path", 225 | args: args{ 226 | key: configPathVar, 227 | }, 228 | want: want{ 229 | evalKey: EvalPathVar, 230 | }, 231 | }, 232 | { 233 | name: "content-type", 234 | args: args{ 235 | key: configContentTypeVar, 236 | }, 237 | want: want{ 238 | evalKey: EvalContentTypeVar, 239 | }, 240 | }, 241 | { 242 | name: "status-code", 243 | args: args{ 244 | key: configStatusCodeVar, 245 | }, 246 | want: want{ 247 | evalKey: EvalStatusCodeVar, 248 | }, 249 | }, 250 | { 251 | name: "$(req.header::)", 252 | args: args{ 253 | key: configReqHeaderVar, 254 | }, 255 | want: want{ 256 | regexEvalKey: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", EvalReqHeaderVar)), 257 | }, 258 | }, 259 | { 260 | name: "$(resp.header::)", 261 | args: args{ 262 | key: configRespHeaderVar, 263 | }, 264 | want: want{ 265 | regexEvalKey: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", EvalRespHeaderVar)), 266 | }, 267 | }, 268 | { 269 | name: "$(cookie::)", 270 | args: args{ 271 | key: configCookieVar, 272 | }, 273 | want: want{ 274 | regexEvalKey: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", EvalCookieVar)), 275 | }, 276 | }, 277 | { 278 | name: "unknown", 279 | args: args{ 280 | key: "$(unknown)", 281 | }, 282 | want: want{ 283 | evalKey: "$(unknown)", 284 | }, 285 | }, 286 | } 287 | 288 | for _, tt := range tests { 289 | t.Run(tt.name, func(t *testing.T) { 290 | s := GetEvalParamName(tt.args.key) 291 | 292 | if tt.want.regexEvalKey != nil { 293 | if !tt.want.regexEvalKey.MatchString(s) { 294 | t.Errorf("GetEvalParamName() = '%s', want '%s'", s, tt.want.regexEvalKey.String()) 295 | } 296 | } else { 297 | if s != tt.want.evalKey { 298 | t.Errorf("GetEvalParamName() = '%s', want '%s'", s, tt.want.evalKey) 299 | } 300 | } 301 | }) 302 | } 303 | } 304 | 305 | func TestParseConfigKeys(t *testing.T) { 306 | type args struct { 307 | key string 308 | } 309 | 310 | type want struct { 311 | configKey string 312 | evalKey string 313 | evalSubKey string 314 | regexEvalKey *regexp.Regexp 315 | } 316 | 317 | tests := []struct { 318 | name string 319 | args args 320 | want want 321 | }{ 322 | { 323 | name: "method", 324 | args: args{ 325 | key: configMethodVar, 326 | }, 327 | want: want{ 328 | configKey: configMethodVar, 329 | evalKey: EvalMethodVar, 330 | }, 331 | }, 332 | { 333 | name: "host", 334 | args: args{ 335 | key: configHostVar, 336 | }, 337 | want: want{ 338 | configKey: configHostVar, 339 | evalKey: EvalHostVar, 340 | }, 341 | }, 342 | { 343 | name: "path", 344 | args: args{ 345 | key: configPathVar, 346 | }, 347 | want: want{ 348 | configKey: configPathVar, 349 | evalKey: EvalPathVar, 350 | }, 351 | }, 352 | { 353 | name: "content-type", 354 | args: args{ 355 | key: configContentTypeVar, 356 | }, 357 | want: want{ 358 | configKey: configContentTypeVar, 359 | evalKey: EvalContentTypeVar, 360 | }, 361 | }, 362 | { 363 | name: "status-code", 364 | args: args{ 365 | key: configStatusCodeVar, 366 | }, 367 | want: want{ 368 | configKey: configStatusCodeVar, 369 | evalKey: EvalStatusCodeVar, 370 | }, 371 | }, 372 | { 373 | name: "$(req.header::)", 374 | args: args{ 375 | key: "$(req.header::X-Data)", 376 | }, 377 | want: want{ 378 | configKey: "$(req.header::X-Data)", 379 | evalSubKey: "X-Data", 380 | regexEvalKey: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", EvalReqHeaderVar)), 381 | }, 382 | }, 383 | { 384 | name: "$(resp.header::)", 385 | args: args{ 386 | key: "$(resp.header::X-Data)", 387 | }, 388 | want: want{ 389 | configKey: "$(resp.header::X-Data)", 390 | evalSubKey: "X-Data", 391 | regexEvalKey: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", EvalRespHeaderVar)), 392 | }, 393 | }, 394 | { 395 | name: "$(cookie::)", 396 | args: args{ 397 | key: "$(cookie::Kratgo)", 398 | }, 399 | want: want{ 400 | configKey: "$(cookie::Kratgo)", 401 | evalSubKey: "Kratgo", 402 | regexEvalKey: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", EvalCookieVar)), 403 | }, 404 | }, 405 | { 406 | name: "unknown", 407 | args: args{ 408 | key: "$(unknown)", 409 | }, 410 | want: want{ 411 | configKey: "", 412 | evalKey: "", 413 | evalSubKey: "", 414 | }, 415 | }, 416 | } 417 | 418 | for _, tt := range tests { 419 | t.Run(tt.name, func(t *testing.T) { 420 | configKey, evalKey, evalSubKey := ParseConfigKeys(tt.args.key) 421 | 422 | if tt.want.configKey != configKey { 423 | t.Errorf("GetEvalParamName()[0] = '%s', want '%s'", configKey, tt.want.configKey) 424 | } 425 | 426 | if tt.want.regexEvalKey != nil { 427 | if !tt.want.regexEvalKey.MatchString(evalKey) { 428 | t.Errorf("GetEvalParamName()[1] = '%s', want '%s'", evalKey, tt.want.regexEvalKey.String()) 429 | } 430 | } else { 431 | if evalKey != tt.want.evalKey { 432 | t.Errorf("GetEvalParamName()[1] = '%s', want '%s'", evalKey, tt.want.evalKey) 433 | } 434 | } 435 | 436 | if tt.want.evalSubKey != evalSubKey { 437 | t.Errorf("GetEvalParamName()[2] = '%s', want '%s'", evalSubKey, tt.want.evalSubKey) 438 | } 439 | }) 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /modules/config/types.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Config ... 4 | type Config struct { 5 | Cache Cache `yaml:"cache"` 6 | Invalidator Invalidator `yaml:"invalidator"` 7 | Proxy Proxy `yaml:"proxy"` 8 | Admin Admin `yaml:"admin"` 9 | 10 | LogLevel string `yaml:"logLevel"` 11 | LogOutput string `yaml:"logOutput"` 12 | } 13 | 14 | // Proxy ... 15 | type Proxy struct { 16 | Addr string `yaml:"addr"` 17 | BackendAddrs []string `yaml:"backendAddrs"` 18 | Response ProxyResponse `yaml:"response"` 19 | Nocache []string `yaml:"nocache"` 20 | } 21 | 22 | // ProxyResponse ... 23 | type ProxyResponse struct { 24 | Headers ProxyResponseHeaders `yaml:"headers"` 25 | } 26 | 27 | // ProxyResponseHeaders ... 28 | type ProxyResponseHeaders struct { 29 | Set []Header `yaml:"set"` 30 | Unset []Header `yaml:"unset"` 31 | } 32 | 33 | // Header ... 34 | type Header struct { 35 | Name string `yaml:"name"` 36 | Value string `yaml:"value"` 37 | When string `yaml:"if"` 38 | } 39 | 40 | // Cache ... 41 | type Cache struct { 42 | TTL int `yaml:"ttl"` 43 | CleanFrequency int `yaml:"cleanFrequency"` 44 | MaxEntries int `yaml:"maxEntries"` 45 | MaxEntrySize int `yaml:"maxEntrySize"` 46 | HardMaxCacheSize int `yaml:"hardMaxCacheSize"` 47 | } 48 | 49 | // Invalidator ... 50 | type Invalidator struct { 51 | MaxWorkers int32 `yaml:"maxWorkers"` 52 | } 53 | 54 | // Admin ... 55 | type Admin struct { 56 | Addr string `yaml:"addr"` 57 | } 58 | -------------------------------------------------------------------------------- /modules/invalidator/const.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | const ( 4 | invTypeHost invType = iota 5 | invTypePath 6 | invTypeHeader 7 | invTypePathHeader 8 | invTypeInvalid 9 | ) 10 | -------------------------------------------------------------------------------- /modules/invalidator/entry.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import "sync" 4 | 5 | var entryPool = sync.Pool{ 6 | New: func() interface{} { 7 | return new(Entry) 8 | }, 9 | } 10 | 11 | // AcquireEntry ... 12 | func AcquireEntry() *Entry { 13 | return entryPool.Get().(*Entry) 14 | } 15 | 16 | // ReleaseEntry ... 17 | func ReleaseEntry(e *Entry) { 18 | e.Reset() 19 | entryPool.Put(e) 20 | } 21 | 22 | // Reset ... 23 | func (h *EntryHeader) Reset() { 24 | h.Key = "" 25 | h.Value = "" 26 | } 27 | 28 | // Reset ... 29 | func (e *Entry) Reset() { 30 | e.Host = "" 31 | e.Path = "" 32 | 33 | e.Header.Reset() 34 | } 35 | -------------------------------------------------------------------------------- /modules/invalidator/entry_test.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func getEntryTest() Entry { 8 | return Entry{ 9 | Host: "www.kratgo.com", 10 | Path: "/fast/", 11 | Header: EntryHeader{ 12 | Key: "X-Data", 13 | Value: "1", 14 | }, 15 | } 16 | } 17 | 18 | func TestAcquireEntry(t *testing.T) { 19 | e := AcquireEntry() 20 | if e == nil { 21 | t.Errorf("AcquireEntry() returns '%v'", nil) 22 | } 23 | } 24 | 25 | func TestReleaseEntry(t *testing.T) { 26 | e := AcquireEntry() 27 | e.Host = "www.kratgo.es" 28 | e.Path = "/fast" 29 | e.Header.Key = "X-Data" 30 | e.Header.Value = "1" 31 | 32 | ReleaseEntry(e) 33 | 34 | if e.Host != "" { 35 | t.Errorf("ReleaseEntry() entry has not been reset") 36 | } 37 | if e.Path != "" { 38 | t.Errorf("ReleaseEntry() entry has not been reset") 39 | } 40 | if e.Header.Key != "" { 41 | t.Errorf("ReleaseEntry() entry has not been reset") 42 | } 43 | if e.Header.Value != "" { 44 | t.Errorf("ReleaseEntry() entry has not been reset") 45 | } 46 | } 47 | 48 | func TestHeader_Reset(t *testing.T) { 49 | e := getEntryTest() 50 | 51 | e.Reset() 52 | 53 | if e.Host != "" { 54 | t.Errorf("Entry.Host has not been reset") 55 | } 56 | 57 | if e.Path != "" { 58 | t.Errorf("Entry.Path has not been reset") 59 | } 60 | 61 | if e.Header.Key != "" { 62 | t.Errorf("Entry.Header.Key has not been reset") 63 | } 64 | 65 | if e.Header.Value != "" { 66 | t.Errorf("Entry.Header.Value has not been reset") 67 | } 68 | } 69 | 70 | func TestEntry_Reset(t *testing.T) { 71 | e := getEntryTest() 72 | 73 | e.Header.Reset() 74 | 75 | if e.Header.Key != "" { 76 | t.Errorf("Entry.Header.Key has not been reset") 77 | } 78 | 79 | if e.Header.Value != "" { 80 | t.Errorf("Entry.Header.Value has not been reset") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /modules/invalidator/errors.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import "errors" 4 | 5 | // ErrEmptyFields ... 6 | var ErrEmptyFields = errors.New("Minimum one mandatory field") 7 | 8 | // ErrMaxWorkersZero ... 9 | var ErrMaxWorkersZero = errors.New("MaxWorkers must be greater than 0") 10 | -------------------------------------------------------------------------------- /modules/invalidator/handler.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/allegro/bigcache/v3" 7 | "github.com/savsgio/gotils/strconv" 8 | "github.com/savsgio/kratgo/modules/cache" 9 | ) 10 | 11 | func (i *Invalidator) deleteCacheKey(cacheKey string) error { 12 | if err := i.cache.Del(cacheKey); err != nil && err != bigcache.ErrEntryNotFound { 13 | return err 14 | } 15 | 16 | return nil 17 | } 18 | 19 | func (i *Invalidator) invalidateByHost(cacheKey string) error { 20 | if err := i.deleteCacheKey(cacheKey); err != nil { 21 | return fmt.Errorf("Could not invalidate cache by host '%s': %v", cacheKey, err) 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func (i *Invalidator) invalidateByPath(cacheKey string, cacheEntry cache.Entry, e Entry) error { 28 | path := strconv.S2B(e.Path) 29 | 30 | if !cacheEntry.HasResponse(path) { 31 | return nil 32 | } 33 | 34 | if cacheEntry.Len() == 1 { 35 | // Only delete the cache data for current key if remaining 1 response, to free memory 36 | if err := i.deleteCacheKey(cacheKey); err != nil { 37 | return fmt.Errorf("Could not invalidate cache by path '%s': %v", e.Path, err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | cacheEntry.DelResponse(path) 44 | 45 | if err := i.cache.Set(cacheKey, cacheEntry); err != nil { 46 | return fmt.Errorf("Could not invalidate cache by path '%s': %v", e.Path, err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (i *Invalidator) invalidateByHeader(cacheKey string, cacheEntry cache.Entry, e Entry) error { 53 | responses := cacheEntry.GetAllResponses() 54 | 55 | for _, resp := range responses { 56 | if !resp.HasHeader(strconv.S2B(e.Header.Key), strconv.S2B(e.Header.Value)) { 57 | continue 58 | } 59 | 60 | if cacheEntry.Len() == 1 { 61 | // Only delete the cache data for current key if remaining 1 response, to free memory 62 | if err := i.deleteCacheKey(cacheKey); err != nil { 63 | return fmt.Errorf("Could not invalidate cache by header '%s = %s': %v", e.Header.Key, e.Header.Value, err) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | cacheEntry.DelResponse(resp.Path) 70 | } 71 | 72 | if err := i.cache.Set(cacheKey, cacheEntry); err != nil { 73 | return fmt.Errorf("Could not invalidate cache by header '%s = %s': %v", e.Header.Key, e.Header.Value, err) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (i *Invalidator) invalidateByPathHeader(cacheKey string, cacheEntry cache.Entry, e Entry) error { 80 | path := strconv.S2B(e.Path) 81 | 82 | resp := cacheEntry.GetResponse(path) 83 | if resp == nil { 84 | return nil 85 | } 86 | 87 | if !resp.HasHeader(strconv.S2B(e.Header.Key), strconv.S2B(e.Header.Value)) { 88 | return nil 89 | } 90 | 91 | if cacheEntry.Len() == 1 { 92 | // Only delete the cache data for current key if remaining 1 response, to free memory 93 | if err := i.deleteCacheKey(cacheKey); err != nil { 94 | return fmt.Errorf("Could not invalidate cache by path '%s' and header '%s = %s': %v", e.Path, e.Header.Key, e.Header.Value, err) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | cacheEntry.DelResponse(path) 101 | 102 | if err := i.cache.Set(cacheKey, cacheEntry); err != nil { 103 | return fmt.Errorf("Could not invalidate cache by path '%s' and header '%s = %s': %v", e.Path, e.Header.Key, e.Header.Value, err) 104 | } 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /modules/invalidator/handler_test.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/savsgio/kratgo/modules/cache" 7 | ) 8 | 9 | func TestInvalidator_invalidateByHost(t *testing.T) { 10 | i, err := New(testConfig()) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | key := "www.kratgo.com" 16 | 17 | resp := cache.AcquireResponse() 18 | resp.Path = []byte("/fast") 19 | 20 | entry := cache.AcquireEntry() 21 | entry.SetResponse(*resp) 22 | 23 | i.cache.Set(key, *entry) 24 | 25 | i.invalidateByHost(key) 26 | 27 | entry.Reset() 28 | 29 | if err := i.cache.Get(key, entry); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if entry.Len() > 0 { 34 | t.Error("The cache has not been invalidate by host") 35 | } 36 | } 37 | 38 | func TestInvalidator_invalidateByPath(t *testing.T) { 39 | i, err := New(testConfig()) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | path := "/fast" 45 | 46 | e := Entry{ 47 | Path: path, 48 | } 49 | 50 | key := "www.kratgo.com" 51 | 52 | resp := cache.AcquireResponse() 53 | resp.Path = []byte(path) 54 | 55 | cacheEntry := cache.AcquireEntry() 56 | cacheEntry.SetResponse(*resp) 57 | 58 | i.cache.Set(key, *cacheEntry) 59 | 60 | if err := i.invalidateByPath(key, *cacheEntry, e); err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | cacheEntry.Reset() 65 | 66 | if err := i.cache.Get(key, cacheEntry); err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | if cacheEntry.HasResponse(resp.Path) { 71 | t.Error("The cache has not been invalidate by path") 72 | } 73 | } 74 | 75 | func TestInvalidator_invalidateByHeader(t *testing.T) { 76 | i, err := New(testConfig()) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | path := "/fast" 82 | headerKey := []byte("kratgo") 83 | headerValue := []byte("fast") 84 | 85 | e := Entry{ 86 | Path: path, 87 | Header: EntryHeader{ 88 | Key: string(headerKey), 89 | Value: string(headerValue), 90 | }, 91 | } 92 | 93 | key := "www.kratgo.com" 94 | 95 | resp := cache.AcquireResponse() 96 | resp.SetHeader(headerKey, headerValue) 97 | 98 | cacheEntry := cache.AcquireEntry() 99 | cacheEntry.SetResponse(*resp) 100 | 101 | i.cache.Set(key, *cacheEntry) 102 | 103 | if err := i.invalidateByHeader(key, *cacheEntry, e); err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | cacheEntry.Reset() 108 | 109 | if err := i.cache.Get(key, cacheEntry); err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | if cacheEntry.HasResponse(resp.Path) { 114 | t.Error("The cache has not been invalidate by header") 115 | } 116 | } 117 | 118 | func TestInvalidator_invalidateByPathHeader(t *testing.T) { 119 | i, err := New(testConfig()) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | path := "/fast" 125 | headerKey := []byte("kratgo") 126 | headerValue := []byte("fast") 127 | 128 | e := Entry{ 129 | Path: path, 130 | Header: EntryHeader{ 131 | Key: string(headerKey), 132 | Value: string(headerValue), 133 | }, 134 | } 135 | 136 | key := "www.kratgo.com" 137 | 138 | resp := cache.AcquireResponse() 139 | resp.Path = []byte(path) 140 | resp.SetHeader(headerKey, headerValue) 141 | 142 | cacheEntry := cache.AcquireEntry() 143 | cacheEntry.SetResponse(*resp) 144 | 145 | i.cache.Set(key, *cacheEntry) 146 | 147 | if err := i.invalidateByPathHeader(key, *cacheEntry, e); err != nil { 148 | t.Fatal(err) 149 | } 150 | 151 | cacheEntry.Reset() 152 | 153 | if err := i.cache.Get(key, cacheEntry); err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | if cacheEntry.HasResponse(resp.Path) { 158 | t.Error("The cache has not been invalidate by path and header") 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /modules/invalidator/invalidator.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | 7 | logger "github.com/savsgio/go-logger/v4" 8 | "github.com/savsgio/kratgo/modules/cache" 9 | ) 10 | 11 | // New ... 12 | func New(cfg Config) (*Invalidator, error) { 13 | if cfg.FileConfig.MaxWorkers == 0 { 14 | return nil, ErrMaxWorkersZero 15 | } 16 | 17 | log := logger.New(cfg.LogLevel, cfg.LogOutput, logger.Field{Key: "type", Value: "invalidator"}) 18 | 19 | i := &Invalidator{ 20 | fileConfig: cfg.FileConfig, 21 | cache: cfg.Cache, 22 | chEntries: make(chan Entry), 23 | log: log, 24 | } 25 | 26 | return i, nil 27 | } 28 | 29 | func (i *Invalidator) invalidationType(e Entry) invType { 30 | if e.Host == "" && e.Path == "" && e.Header.Key == "" { 31 | return invTypeInvalid 32 | } 33 | 34 | if e.Path != "" { 35 | if e.Header.Key != "" { 36 | return invTypePathHeader 37 | } 38 | 39 | return invTypePath 40 | } 41 | 42 | if e.Header.Key != "" { 43 | return invTypeHeader 44 | } 45 | 46 | return invTypeHost 47 | } 48 | 49 | func (i *Invalidator) invalidate(invalidationType invType, key string, entry cache.Entry, e Entry) error { 50 | switch invalidationType { 51 | case invTypeHost: 52 | return i.invalidateByHost(key) 53 | case invTypePath: 54 | return i.invalidateByPath(key, entry, e) 55 | case invTypeHeader: 56 | return i.invalidateByHeader(key, entry, e) 57 | case invTypePathHeader: 58 | return i.invalidateByPathHeader(key, entry, e) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (i *Invalidator) invalidateAll(invalidationType invType, e Entry) { 65 | atomic.AddInt32(&i.activeWorkers, 1) 66 | defer atomic.AddInt32(&i.activeWorkers, -1) 67 | 68 | entry := cache.AcquireEntry() 69 | iter := i.cache.Iterator() 70 | 71 | for iter.SetNext() { 72 | v, err := iter.Value() 73 | if err != nil { 74 | i.log.Errorf("Could not get value from iterator: %v", err) 75 | continue 76 | } 77 | 78 | if err = cache.Unmarshal(entry, v.Value()); err != nil { 79 | i.log.Errorf("Could not decode cache value: %v", err) 80 | continue 81 | } 82 | 83 | if err = i.invalidate(invalidationType, v.Key(), *entry, e); err != nil { 84 | i.log.Errorf("Could not invalidate '%v': %v", *entry, err) 85 | } 86 | 87 | entry.Reset() 88 | } 89 | 90 | cache.ReleaseEntry(entry) 91 | } 92 | 93 | func (i *Invalidator) invalidateHost(invalidationType invType, e Entry) { 94 | atomic.AddInt32(&i.activeWorkers, 1) 95 | defer atomic.AddInt32(&i.activeWorkers, -1) 96 | 97 | key := e.Host 98 | entry := cache.AcquireEntry() 99 | 100 | err := i.cache.Get(key, entry) 101 | if err != nil { 102 | i.log.Errorf("Could not get responses from cache by key '%s': %v", key, err) 103 | } else if entry.Len() == 0 { 104 | return 105 | } 106 | 107 | if err = i.invalidate(invalidationType, key, *entry, e); err != nil { 108 | i.log.Error(err) 109 | } 110 | 111 | cache.ReleaseEntry(entry) 112 | } 113 | 114 | func (i *Invalidator) waitAvailableWorkers() { 115 | for atomic.LoadInt32(&i.activeWorkers) > i.fileConfig.MaxWorkers { 116 | time.Sleep(100 * time.Millisecond) 117 | } 118 | } 119 | 120 | // Add .. 121 | func (i *Invalidator) Add(e Entry) error { 122 | if t := i.invalidationType(e); t == invTypeInvalid { 123 | return ErrEmptyFields 124 | } 125 | 126 | i.chEntries <- e 127 | 128 | return nil 129 | } 130 | 131 | // Start ... 132 | func (i *Invalidator) Start() { 133 | for e := range i.chEntries { 134 | invalidationType := i.invalidationType(e) 135 | 136 | i.waitAvailableWorkers() 137 | 138 | if e.Host != "" { 139 | go i.invalidateHost(invalidationType, e) 140 | } else { 141 | go i.invalidateAll(invalidationType, e) 142 | } 143 | 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /modules/invalidator/invalidator_test.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | logger "github.com/savsgio/go-logger/v4" 10 | "github.com/savsgio/kratgo/modules/cache" 11 | "github.com/savsgio/kratgo/modules/config" 12 | ) 13 | 14 | var testCache *cache.Cache 15 | 16 | func init() { 17 | c, err := cache.New(cache.Config{ 18 | FileConfig: fileConfigCache(), 19 | LogLevel: logger.ERROR, 20 | LogOutput: os.Stderr, 21 | }) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | testCache = c 27 | } 28 | 29 | func fileConfigInvalidator() config.Invalidator { 30 | return config.Invalidator{ 31 | MaxWorkers: 1, 32 | } 33 | } 34 | 35 | func fileConfigCache() config.Cache { 36 | return config.Cache{ 37 | TTL: 10, 38 | CleanFrequency: 5, 39 | MaxEntries: 5, 40 | MaxEntrySize: 20, 41 | HardMaxCacheSize: 30, 42 | } 43 | } 44 | 45 | func testConfig() Config { 46 | testCache.Reset() 47 | 48 | return Config{ 49 | FileConfig: fileConfigInvalidator(), 50 | Cache: testCache, 51 | LogLevel: logger.ERROR, 52 | LogOutput: os.Stderr, 53 | } 54 | } 55 | 56 | func TestInvalidator_New(t *testing.T) { 57 | type args struct { 58 | cfg Config 59 | } 60 | 61 | type want struct { 62 | err bool 63 | } 64 | 65 | logLevel := logger.FATAL 66 | logOutput := os.Stderr 67 | 68 | tests := []struct { 69 | name string 70 | args args 71 | want want 72 | }{ 73 | { 74 | name: "Ok", 75 | args: args{ 76 | cfg: Config{ 77 | FileConfig: config.Invalidator{ 78 | MaxWorkers: 1, 79 | }, 80 | Cache: testCache, 81 | LogLevel: logLevel, 82 | LogOutput: logOutput, 83 | }, 84 | }, 85 | want: want{ 86 | err: false, 87 | }, 88 | }, 89 | { 90 | name: "Error", 91 | args: args{ 92 | cfg: Config{ 93 | Cache: testCache, 94 | LogLevel: logLevel, 95 | LogOutput: logOutput, 96 | }, 97 | }, 98 | want: want{ 99 | err: true, 100 | }, 101 | }, 102 | } 103 | 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | i, err := New(tt.args.cfg) 107 | if (err != nil) != tt.want.err { 108 | t.Errorf("New() Unexpected error: %v", err) 109 | } 110 | 111 | if tt.want.err { 112 | return 113 | } 114 | 115 | if !reflect.DeepEqual(i.fileConfig, tt.args.cfg.FileConfig) { 116 | t.Errorf("New() fileConfig == '%v', want '%v'", i.fileConfig, tt.args.cfg.FileConfig) 117 | } 118 | 119 | if reflect.ValueOf(i.cache).Pointer() != reflect.ValueOf(testCache).Pointer() { 120 | t.Errorf("New() fileConfig == '%p', want '%p'", i.cache, testCache) 121 | } 122 | 123 | if i.chEntries == nil { 124 | t.Errorf("New() chEntries is '%v'", nil) 125 | } 126 | 127 | if i.log == nil { 128 | t.Errorf("New() log is '%v'", nil) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | func TestInvalidator_invalidationType(t *testing.T) { 135 | type args struct { 136 | e Entry 137 | } 138 | type want struct { 139 | t invType 140 | } 141 | 142 | tests := []struct { 143 | name string 144 | args args 145 | want want 146 | }{ 147 | { 148 | name: "Host", 149 | args: args{ 150 | e: Entry{ 151 | Host: "www.kratgo.com", 152 | }, 153 | }, 154 | want: want{ 155 | t: invTypeHost, 156 | }, 157 | }, 158 | { 159 | name: "Path", 160 | args: args{ 161 | e: Entry{ 162 | Path: "/fast", 163 | }, 164 | }, 165 | want: want{ 166 | t: invTypePath, 167 | }, 168 | }, 169 | { 170 | name: "Header", 171 | args: args{ 172 | e: Entry{ 173 | Header: EntryHeader{ 174 | Key: "X-Data", 175 | Value: "Fast", 176 | }, 177 | }, 178 | }, 179 | want: want{ 180 | t: invTypeHeader, 181 | }, 182 | }, 183 | { 184 | name: "PathHeader", 185 | args: args{ 186 | e: Entry{ 187 | Path: "/lightweight", 188 | Header: EntryHeader{ 189 | Key: "X-Data", 190 | Value: "Fast", 191 | }, 192 | }, 193 | }, 194 | want: want{ 195 | t: invTypePathHeader, 196 | }, 197 | }, 198 | { 199 | name: "Invalid", 200 | args: args{ 201 | e: Entry{}, 202 | }, 203 | want: want{ 204 | t: invTypeInvalid, 205 | }, 206 | }, 207 | } 208 | 209 | i, err := New(testConfig()) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | for _, tt := range tests { 215 | t.Run(tt.name, func(t *testing.T) { 216 | if got := i.invalidationType(tt.args.e); got != tt.want.t { 217 | t.Errorf("Invalidator.invalidationType() = %d, want %d", got, tt.want.t) 218 | } 219 | }) 220 | } 221 | } 222 | 223 | func TestInvalidator_invalidate(t *testing.T) { 224 | type args struct { 225 | invalidationType invType 226 | entry cache.Entry 227 | e Entry 228 | } 229 | 230 | type want struct { 231 | foundInCache bool 232 | } 233 | 234 | host := "www.kratgo.com" 235 | responses := []cache.Response{ 236 | { 237 | Path: []byte("/fast"), 238 | Body: []byte("Kratgo is not slow"), 239 | Headers: []cache.ResponseHeader{ 240 | {Key: []byte("X-Data"), Value: []byte("1")}, 241 | }, 242 | }, 243 | } 244 | 245 | tests := []struct { 246 | name string 247 | args args 248 | want want 249 | }{ 250 | { 251 | name: "Host", 252 | args: args{ 253 | invalidationType: invTypeHost, 254 | entry: cache.Entry{ 255 | Responses: responses, 256 | }, 257 | e: Entry{ 258 | Host: host, 259 | }, 260 | }, 261 | want: want{ 262 | foundInCache: false, 263 | }, 264 | }, 265 | { 266 | name: "Path", 267 | args: args{ 268 | invalidationType: invTypePath, 269 | entry: cache.Entry{ 270 | Responses: responses, 271 | }, 272 | e: Entry{ 273 | Host: host, 274 | Path: "/fast", 275 | }, 276 | }, 277 | want: want{ 278 | foundInCache: false, 279 | }, 280 | }, 281 | { 282 | name: "PathHeader", 283 | args: args{ 284 | invalidationType: invTypePathHeader, 285 | entry: cache.Entry{ 286 | Responses: responses, 287 | }, 288 | e: Entry{ 289 | Host: host, 290 | Path: "/fast", 291 | Header: EntryHeader{ 292 | Key: "X-Data", 293 | Value: "1", 294 | }, 295 | }, 296 | }, 297 | want: want{ 298 | foundInCache: false, 299 | }, 300 | }, 301 | { 302 | name: "Header", 303 | args: args{ 304 | invalidationType: invTypeHeader, 305 | entry: cache.Entry{ 306 | Responses: responses, 307 | }, 308 | e: Entry{ 309 | Host: host, 310 | Header: EntryHeader{ 311 | Key: "X-Data", 312 | Value: "1", 313 | }, 314 | }, 315 | }, 316 | want: want{ 317 | foundInCache: false, 318 | }, 319 | }, 320 | { 321 | name: "Invalid", 322 | args: args{ 323 | invalidationType: invTypeInvalid, 324 | entry: cache.Entry{ 325 | Responses: responses, 326 | }, 327 | e: Entry{ 328 | Host: "www.fake.com", 329 | }, 330 | }, 331 | want: want{ 332 | foundInCache: true, 333 | }, 334 | }, 335 | } 336 | 337 | i, err := New(testConfig()) 338 | if err != nil { 339 | t.Fatal(err) 340 | } 341 | 342 | for _, tt := range tests { 343 | t.Run(tt.name, func(t *testing.T) { 344 | key := tt.args.e.Host 345 | 346 | if err := i.cache.Set(key, tt.args.entry); err != nil { 347 | t.Fatal(err) 348 | } 349 | 350 | if err := i.invalidate(tt.args.invalidationType, key, tt.args.entry, tt.args.e); err != nil { 351 | t.Fatal(err) 352 | } 353 | 354 | cacheEntry := cache.AcquireEntry() 355 | if err := i.cache.Get(key, cacheEntry); err != nil { 356 | t.Fatal(err) 357 | } 358 | 359 | length := cacheEntry.Len() 360 | if length > 0 && !tt.want.foundInCache { 361 | t.Errorf("Invalidator.invalidate() cache has not been invalidate, type = '%d'", tt.args.invalidationType) 362 | } else if length == 0 && tt.want.foundInCache { 363 | t.Errorf("Invalidator.invalidate() cache has been invalidate, type = '%d'", tt.args.invalidationType) 364 | } 365 | }) 366 | } 367 | } 368 | 369 | func TestInvalidator_invalidateAll(t *testing.T) { 370 | type args struct { 371 | invalidationType invType 372 | entry cache.Entry 373 | e Entry 374 | } 375 | 376 | type want struct { 377 | foundInCache bool 378 | } 379 | 380 | path := []byte("/fast") 381 | 382 | host1 := "www.kratgo.com" 383 | responses1 := []cache.Response{ 384 | { 385 | Path: path, 386 | Body: []byte("Kratgo is not slow"), 387 | Headers: []cache.ResponseHeader{ 388 | {Key: []byte("X-Data"), Value: []byte("1")}, 389 | }, 390 | }, 391 | } 392 | 393 | host2 := "www.cache-fast.com" 394 | responses2 := []cache.Response{ 395 | { 396 | Path: path, 397 | Body: []byte("Kratgo is not slow"), 398 | Headers: []cache.ResponseHeader{ 399 | {Key: []byte("X-Data"), Value: []byte("1")}, 400 | }, 401 | }, 402 | } 403 | 404 | host3 := "www.high-performance.com" 405 | responses3 := []cache.Response{ 406 | { 407 | Path: []byte("/kratgo"), 408 | Body: []byte("Kratgo is not slow"), 409 | Headers: []cache.ResponseHeader{ 410 | {Key: []byte("X-Data"), Value: []byte("1")}, 411 | }, 412 | }, 413 | } 414 | 415 | i, err := New(testConfig()) 416 | if err != nil { 417 | t.Fatal(err) 418 | } 419 | 420 | i.cache.Set(host1, cache.Entry{Responses: responses1}) 421 | i.cache.Set(host2, cache.Entry{Responses: responses2}) 422 | i.cache.Set(host3, cache.Entry{Responses: responses3}) 423 | 424 | i.invalidateAll(invTypePath, Entry{Path: string(path)}) 425 | 426 | wantLength := 1 427 | length := i.cache.Len() 428 | if length != wantLength { 429 | t.Errorf("Invalidator.invalidateAll() cache length == '%d', want '%d'", length, wantLength) 430 | } 431 | } 432 | 433 | func TestInvalidator_invalidateHost(t *testing.T) { 434 | type cacheData struct { 435 | host string 436 | responses []cache.Response 437 | } 438 | 439 | path := []byte("/fast") 440 | 441 | host1 := "www.kratgo.com" 442 | responses1 := []cache.Response{ 443 | { 444 | Path: path, 445 | Body: []byte("Kratgo is not slow"), 446 | Headers: []cache.ResponseHeader{ 447 | {Key: []byte("X-Data"), Value: []byte("1")}, 448 | }, 449 | }, 450 | } 451 | 452 | host2 := "www.cache-fast.com" 453 | responses2 := []cache.Response{ 454 | { 455 | Path: path, 456 | Body: []byte("Kratgo is not slow"), 457 | Headers: []cache.ResponseHeader{ 458 | {Key: []byte("X-Data"), Value: []byte("1")}, 459 | }, 460 | }, 461 | } 462 | 463 | i, err := New(testConfig()) 464 | if err != nil { 465 | t.Fatal(err) 466 | } 467 | 468 | i.cache.Set(host1, cache.Entry{Responses: responses1}) 469 | i.cache.Set(host2, cache.Entry{Responses: responses2}) 470 | 471 | i.invalidateHost(invTypePath, Entry{Host: host1, Path: string(path)}) 472 | 473 | wantLength := 1 474 | length := i.cache.Len() 475 | if length != wantLength { 476 | t.Errorf("Invalidator.invalidateAll() cache length == '%d', want '%d'", length, wantLength) 477 | } 478 | } 479 | 480 | // func TestInvalidator_waitAvailableWorkers(t *testing.T) { 481 | // } 482 | 483 | func TestInvalidator_Add(t *testing.T) { 484 | type args struct { 485 | entry Entry 486 | } 487 | type want struct { 488 | err error 489 | } 490 | 491 | tests := []struct { 492 | name string 493 | args args 494 | want want 495 | }{ 496 | { 497 | name: "Ok", 498 | args: args{ 499 | entry: Entry{ 500 | Host: "www.kratgo.com", 501 | Path: "/fast", 502 | }, 503 | }, 504 | want: want{ 505 | err: nil, 506 | }, 507 | }, 508 | { 509 | name: "Error", 510 | args: args{ 511 | entry: Entry{}, 512 | }, 513 | want: want{ 514 | err: ErrEmptyFields, 515 | }, 516 | }, 517 | } 518 | 519 | i, err := New(testConfig()) 520 | if err != nil { 521 | t.Fatal(err) 522 | } 523 | 524 | go i.Start() 525 | 526 | for _, tt := range tests { 527 | t.Run(tt.name, func(t *testing.T) { 528 | err := i.Add(tt.args.entry) 529 | 530 | if err != tt.want.err { 531 | t.Errorf("Invalidator.Add() error = %v, want %v", err, tt.want.err) 532 | } 533 | }) 534 | } 535 | } 536 | 537 | func TestInvalidator_Start(t *testing.T) { 538 | key := "www.kratgo.com" 539 | path := "/fast" 540 | 541 | entry := Entry{ 542 | Host: key, 543 | Path: path, 544 | } 545 | 546 | i, err := New(testConfig()) 547 | if err != nil { 548 | t.Fatal(err) 549 | } 550 | i.chEntries = make(chan Entry, 1) 551 | 552 | cacheEntry := cache.AcquireEntry() 553 | cacheEntry.SetResponse(cache.Response{Path: []byte(path)}) 554 | 555 | if err := i.cache.Set(key, *cacheEntry); err != nil { 556 | t.Fatal(err) 557 | } 558 | 559 | go i.Add(entry) 560 | 561 | go i.Start() 562 | 563 | time.Sleep(200 * time.Millisecond) 564 | 565 | if i.cache.Len() > 0 { 566 | t.Error("Invalidator.Start() invalidator has not been start") 567 | } 568 | } 569 | -------------------------------------------------------------------------------- /modules/invalidator/types.go: -------------------------------------------------------------------------------- 1 | package invalidator 2 | 3 | import ( 4 | "io" 5 | 6 | logger "github.com/savsgio/go-logger/v4" 7 | "github.com/savsgio/kratgo/modules/cache" 8 | "github.com/savsgio/kratgo/modules/config" 9 | ) 10 | 11 | // Config ... 12 | type Config struct { 13 | FileConfig config.Invalidator 14 | Cache *cache.Cache 15 | 16 | LogLevel logger.Level 17 | LogOutput io.Writer 18 | } 19 | 20 | // Invalidator ... 21 | type Invalidator struct { 22 | fileConfig config.Invalidator 23 | 24 | cache *cache.Cache 25 | 26 | activeWorkers int32 27 | 28 | chEntries chan Entry 29 | log *logger.Logger 30 | } 31 | 32 | // EntryHeader ... 33 | type EntryHeader struct { 34 | Key string `json:"key"` 35 | Value string `json:"value"` 36 | } 37 | 38 | // Entry ... 39 | type Entry struct { 40 | Host string `json:"host"` 41 | Path string `json:"path"` 42 | Header EntryHeader `json:"header"` 43 | } 44 | 45 | type invType int 46 | -------------------------------------------------------------------------------- /modules/proxy/const.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | const proxyReqHeaderKey = "X-Kratgo-Cache" 4 | const proxyReqHeaderValue = "true" 5 | 6 | const headerLocation = "Location" 7 | const headerContentEncoding = "Content-Encoding" 8 | 9 | const ( 10 | setHeaderAction typeHeaderAction = iota 11 | unsetHeaderAction 12 | ) 13 | -------------------------------------------------------------------------------- /modules/proxy/eval_params.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "sync" 4 | 5 | var evalParamsPool = sync.Pool{ 6 | New: func() interface{} { 7 | return &evalParams{ 8 | p: make(map[string]interface{}), 9 | } 10 | }, 11 | } 12 | 13 | func acquireEvalParams() *evalParams { 14 | return evalParamsPool.Get().(*evalParams) 15 | } 16 | 17 | func releaseEvalParams(ep *evalParams) { 18 | ep.reset() 19 | evalParamsPool.Put(ep) 20 | } 21 | 22 | func (ep *evalParams) set(k string, v interface{}) { 23 | ep.p[k] = v 24 | } 25 | 26 | func (ep *evalParams) get(k string) (interface{}, bool) { 27 | v, ok := ep.p[k] 28 | return v, ok 29 | } 30 | 31 | func (ep *evalParams) all() map[string]interface{} { 32 | return ep.p 33 | } 34 | 35 | func (ep *evalParams) del(k string) { 36 | delete(ep.p, k) 37 | } 38 | 39 | func (ep *evalParams) reset() { 40 | for k := range ep.p { 41 | ep.del(k) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modules/proxy/eval_params_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAcquireEvalParams(t *testing.T) { 9 | ep := acquireEvalParams() 10 | if ep == nil { 11 | t.Errorf("acquireEvalParams() returns '%v'", nil) 12 | } 13 | } 14 | 15 | func TestReleaseEvalParams(t *testing.T) { 16 | ep := acquireEvalParams() 17 | ep.set("key", "value") 18 | 19 | releaseEvalParams(ep) 20 | 21 | if len(ep.p) > 0 { 22 | t.Errorf("releaseEvalParams() entry has not been reset") 23 | } 24 | } 25 | 26 | func Test_evalParams_set(t *testing.T) { 27 | ep := acquireEvalParams() 28 | 29 | k := "Kratgo" 30 | v := "fast" 31 | 32 | ep.set(k, v) 33 | 34 | val, ok := ep.p[k] 35 | if !ok { 36 | t.Errorf("evalParams.set() the key '%s' not found", k) 37 | } 38 | 39 | if val.(string) != v { 40 | t.Errorf("evalParams.set() value == '%s', want '%s'", val, v) 41 | } 42 | } 43 | 44 | func Test_evalParams_get(t *testing.T) { 45 | ep := acquireEvalParams() 46 | 47 | k := "Kratgo" 48 | v := "fast" 49 | 50 | ep.set(k, v) 51 | 52 | val, ok := ep.get(k) 53 | if !ok { 54 | t.Errorf("evalParams.set() the key '%s' not found", k) 55 | } 56 | 57 | if val.(string) != v { 58 | t.Errorf("evalParams.set() value == '%s', want '%s'", val, v) 59 | } 60 | } 61 | 62 | func Test_evalParams_all(t *testing.T) { 63 | ep := acquireEvalParams() 64 | 65 | data := map[string]interface{}{ 66 | "Kratgo": "fast", 67 | "slow": false, 68 | } 69 | for k, v := range data { 70 | ep.set(k, v) 71 | } 72 | 73 | all := ep.all() 74 | 75 | if !reflect.DeepEqual(data, all) { 76 | t.Errorf("evalParams.all() == '%v', want '%v'", all, data) 77 | } 78 | } 79 | 80 | func Test_evalParams_del(t *testing.T) { 81 | ep := acquireEvalParams() 82 | 83 | expected := map[string]interface{}{ 84 | "Kratgo": "fast", 85 | } 86 | keyToDelete := "slow" 87 | 88 | data := map[string]interface{}{ 89 | "Kratgo": "fast", 90 | keyToDelete: false, 91 | } 92 | for k, v := range data { 93 | ep.set(k, v) 94 | } 95 | 96 | ep.del(keyToDelete) 97 | 98 | all := ep.all() 99 | 100 | if !reflect.DeepEqual(all, expected) { 101 | t.Errorf("evalParams.all() == '%v', want '%v'", all, expected) 102 | } 103 | } 104 | 105 | func Test_evalParams_reset(t *testing.T) { 106 | ep := acquireEvalParams() 107 | 108 | data := map[string]interface{}{ 109 | "Kratgo": "fast", 110 | "slow": false, 111 | } 112 | for k, v := range data { 113 | ep.set(k, v) 114 | } 115 | 116 | ep.reset() 117 | 118 | if len(ep.p) > 0 { 119 | t.Errorf("evalParams.reset() not reset, current value: %v", ep.p) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /modules/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | logger "github.com/savsgio/go-logger/v4" 9 | "github.com/savsgio/govaluate/v3" 10 | "github.com/savsgio/kratgo/modules/cache" 11 | "github.com/savsgio/kratgo/modules/config" 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | // New ... 16 | func New(cfg Config) (*Proxy, error) { 17 | if len(cfg.FileConfig.BackendAddrs) == 0 { 18 | return nil, fmt.Errorf("Proxy.BackendAddrs configuration is mandatory") 19 | } 20 | 21 | p := new(Proxy) 22 | p.fileConfig = cfg.FileConfig 23 | 24 | log := logger.New(cfg.LogLevel, cfg.LogOutput, logger.Field{Key: "type", Value: "proxy"}) 25 | 26 | p.server = &fasthttp.Server{ 27 | Handler: p.handler, 28 | Name: "Kratgo", 29 | Logger: log, 30 | } 31 | 32 | p.cache = cfg.Cache 33 | p.httpScheme = cfg.HTTPScheme 34 | p.log = log 35 | 36 | for _, addr := range p.fileConfig.BackendAddrs { 37 | p.backends = append(p.backends, &fasthttp.HostClient{Addr: addr}) 38 | } 39 | p.totalBackends = len(p.backends) 40 | 41 | p.tools = sync.Pool{ 42 | New: func() interface{} { 43 | return &proxyTools{ 44 | params: acquireEvalParams(), 45 | entry: cache.AcquireEntry(), 46 | } 47 | }, 48 | } 49 | 50 | if err := p.parseNocacheRules(); err != nil { 51 | return nil, err 52 | } 53 | 54 | if err := p.parseHeadersRules(setHeaderAction, p.fileConfig.Response.Headers.Set); err != nil { 55 | return nil, err 56 | } 57 | 58 | if err := p.parseHeadersRules(unsetHeaderAction, p.fileConfig.Response.Headers.Unset); err != nil { 59 | return nil, err 60 | } 61 | 62 | return p, nil 63 | } 64 | 65 | func (p *Proxy) acquireTools() *proxyTools { 66 | return p.tools.Get().(*proxyTools) 67 | } 68 | 69 | func (p *Proxy) releaseTools(pt *proxyTools) { 70 | pt.params.reset() 71 | pt.entry.Reset() 72 | 73 | p.tools.Put(pt) 74 | } 75 | 76 | func (p *Proxy) getBackend() fetcher { 77 | if p.totalBackends == 1 { 78 | return p.backends[0] 79 | } 80 | 81 | p.mu.Lock() 82 | 83 | if p.currentBackend >= p.totalBackends-1 { 84 | p.currentBackend = 0 85 | } else { 86 | p.currentBackend++ 87 | } 88 | 89 | backend := p.backends[p.currentBackend] 90 | 91 | p.mu.Unlock() 92 | 93 | return backend 94 | } 95 | 96 | func (p *Proxy) newEvaluableExpression(rule string) (*govaluate.EvaluableExpression, []ruleParam, error) { 97 | params := make([]ruleParam, 0) 98 | 99 | for config.ConfigVarRegex.MatchString(rule) { 100 | configKey, evalKey, evalSubKey := config.ParseConfigKeys(rule) 101 | if configKey == "" && evalKey == "" && evalSubKey == "" { 102 | return nil, nil, fmt.Errorf("Invalid condition: %s", rule) 103 | } 104 | 105 | rule = strings.Replace(rule, configKey, evalKey, -1) 106 | params = append(params, ruleParam{name: evalKey, subKey: evalSubKey}) 107 | } 108 | 109 | expr, err := govaluate.NewEvaluableExpression(rule) 110 | return expr, params, err 111 | } 112 | 113 | func (p *Proxy) parseNocacheRules() error { 114 | for _, ncRule := range p.fileConfig.Nocache { 115 | r := rule{} 116 | 117 | expr, params, err := p.newEvaluableExpression(ncRule) 118 | if err != nil { 119 | return fmt.Errorf("Could not get the evaluable expression for rule '%s': %v", ncRule, err) 120 | } 121 | r.expr = expr 122 | r.params = append(r.params, params...) 123 | 124 | p.nocacheRules = append(p.nocacheRules, r) 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func (p *Proxy) parseHeadersRules(action typeHeaderAction, headers []config.Header) error { 131 | for _, h := range headers { 132 | r := headerRule{action: action, name: h.Name} 133 | 134 | if h.When != "" { 135 | expr, params, err := p.newEvaluableExpression(h.When) 136 | if err != nil { 137 | return fmt.Errorf("Could not get the evaluable expression for rule '%s': %v", h.When, err) 138 | } 139 | r.expr = expr 140 | r.params = append(r.params, params...) 141 | } 142 | 143 | if action == setHeaderAction { 144 | _, evalKey, evalSubKey := config.ParseConfigKeys(h.Value) 145 | if evalKey != "" { 146 | r.value.value = evalKey 147 | r.value.subKey = evalSubKey 148 | } else { 149 | r.value.value = h.Value 150 | } 151 | } 152 | 153 | p.headersRules = append(p.headersRules, r) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func (p *Proxy) saveBackendResponse(cacheKey, path []byte, resp *fasthttp.Response, entry *cache.Entry) error { 160 | r := cache.AcquireResponse() 161 | r.Path = append(r.Path, path...) 162 | r.Body = append(r.Body, resp.Body()...) 163 | 164 | resp.Header.VisitAll(func(k, v []byte) { 165 | r.SetHeader(k, v) 166 | }) 167 | 168 | entry.SetResponse(*r) 169 | 170 | if err := p.cache.SetBytes(cacheKey, *entry); err != nil { 171 | return fmt.Errorf("Could not save response in cache for key '%s': %v", cacheKey, err) 172 | } 173 | 174 | cache.ReleaseResponse(r) 175 | 176 | return nil 177 | } 178 | 179 | func (p *Proxy) fetchFromBackend(cacheKey, path []byte, ctx *fasthttp.RequestCtx, pt *proxyTools) error { 180 | p.log.Debugf("%s - %s", ctx.Method(), ctx.Path()) 181 | 182 | ctx.Request.Header.Set(proxyReqHeaderKey, proxyReqHeaderValue) 183 | for _, header := range hopHeaders { 184 | ctx.Request.Header.Del(header) 185 | } 186 | 187 | if err := p.getBackend().Do(&ctx.Request, &ctx.Response); err != nil { 188 | return fmt.Errorf("Could not fetch response from backend: %v", err) 189 | } 190 | 191 | if err := processHeaderRules(ctx, p.headersRules, pt.params); err != nil { 192 | return fmt.Errorf("Could not process headers rules: %v", err) 193 | } 194 | 195 | location := ctx.Response.Header.Peek(headerLocation) 196 | if len(location) > 0 { 197 | return nil 198 | } 199 | 200 | noCache, err := checkIfNoCache(ctx, p.nocacheRules, pt.params) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | if noCache || ctx.Response.StatusCode() != fasthttp.StatusOK { 206 | return nil 207 | } 208 | 209 | return p.saveBackendResponse(cacheKey, path, &ctx.Response, pt.entry) 210 | } 211 | 212 | func (p *Proxy) handler(ctx *fasthttp.RequestCtx) { 213 | pt := p.acquireTools() 214 | 215 | path := ctx.URI().PathOriginal() 216 | cacheKey := ctx.Host() 217 | 218 | if noCache, err := checkIfNoCache(ctx, p.nocacheRules, pt.params); err != nil { 219 | ctx.Error(err.Error(), fasthttp.StatusInternalServerError) 220 | p.log.Error(err) 221 | 222 | } else if !noCache { 223 | if err := p.cache.GetBytes(cacheKey, pt.entry); err != nil { 224 | ctx.Error(err.Error(), fasthttp.StatusInternalServerError) 225 | p.log.Errorf("Could not get data from cache with key '%s': %v", cacheKey, err) 226 | 227 | } else if r := pt.entry.GetResponse(path); r != nil { 228 | ctx.SetBody(r.Body) 229 | for _, h := range r.Headers { 230 | ctx.Response.Header.SetCanonical(h.Key, h.Value) 231 | } 232 | 233 | p.releaseTools(pt) 234 | return 235 | } 236 | } 237 | 238 | if err := p.fetchFromBackend(cacheKey, path, ctx, pt); err != nil { 239 | ctx.Error(err.Error(), fasthttp.StatusInternalServerError) 240 | p.log.Error(err) 241 | } 242 | 243 | p.releaseTools(pt) 244 | } 245 | 246 | // ListenAndServe ... 247 | func (p *Proxy) ListenAndServe() error { 248 | p.log.Infof("Listening on: %s://%s/", p.httpScheme, p.fileConfig.Addr) 249 | 250 | return p.server.ListenAndServe(p.fileConfig.Addr) 251 | } 252 | -------------------------------------------------------------------------------- /modules/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "testing" 13 | "time" 14 | 15 | logger "github.com/savsgio/go-logger/v4" 16 | "github.com/savsgio/gotils/strconv" 17 | "github.com/savsgio/kratgo/modules/cache" 18 | "github.com/savsgio/kratgo/modules/config" 19 | "github.com/valyala/fasthttp" 20 | ) 21 | 22 | type mockServer struct { 23 | addr string 24 | listenAndServeCalled bool 25 | 26 | mu sync.RWMutex 27 | } 28 | 29 | type mockBackend struct { 30 | called bool 31 | 32 | body []byte 33 | headers map[string][]byte 34 | statusCode int 35 | err error 36 | } 37 | 38 | func (mock *mockBackend) Do(req *fasthttp.Request, resp *fasthttp.Response) error { 39 | mock.called = true 40 | 41 | resp.SetBody(mock.body) 42 | resp.SetStatusCode(mock.statusCode) 43 | 44 | for k, v := range mock.headers { 45 | resp.Header.SetCanonical(strconv.S2B(k), v) 46 | } 47 | 48 | return mock.err 49 | } 50 | 51 | var testCache *cache.Cache 52 | 53 | func init() { 54 | c, err := cache.New(cache.Config{ 55 | FileConfig: config.Cache{ 56 | TTL: 10, 57 | CleanFrequency: 5, 58 | MaxEntries: 5, 59 | MaxEntrySize: 20, 60 | HardMaxCacheSize: 30, 61 | }, 62 | LogLevel: logger.ERROR, 63 | LogOutput: os.Stderr, 64 | }) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | testCache = c 70 | } 71 | 72 | func (mock *mockServer) ListenAndServe(addr string) error { 73 | mock.mu.Lock() 74 | mock.addr = addr 75 | mock.listenAndServeCalled = true 76 | mock.mu.Unlock() 77 | 78 | time.Sleep(250 * time.Millisecond) 79 | 80 | return nil 81 | } 82 | 83 | func testConfig() Config { 84 | testCache.Reset() 85 | 86 | return Config{ 87 | FileConfig: config.Proxy{ 88 | Addr: "localhost:8000", 89 | BackendAddrs: []string{"localhost:9990", "localhost:9991", "localhost:9993", "localhost:9994"}, 90 | }, 91 | Cache: testCache, 92 | LogLevel: logger.FATAL, 93 | LogOutput: os.Stderr, 94 | } 95 | } 96 | 97 | func TestProxy_New(t *testing.T) { 98 | type args struct { 99 | cfg Config 100 | } 101 | 102 | type want struct { 103 | err bool 104 | } 105 | 106 | logLevel := logger.FATAL 107 | logOutput := os.Stderr 108 | httpScheme := "http" 109 | 110 | tests := []struct { 111 | name string 112 | args args 113 | want want 114 | }{ 115 | { 116 | name: "Ok", 117 | args: args{ 118 | cfg: Config{ 119 | FileConfig: config.Proxy{ 120 | Addr: "localhost:9999", 121 | BackendAddrs: []string{"localhost:8881", "localhost:8882"}, 122 | Response: config.ProxyResponse{ 123 | Headers: config.ProxyResponseHeaders{ 124 | Set: []config.Header{ 125 | {Name: "X-Kratgo", Value: "true", When: "$(resp.header::X-Data) == '1'"}, 126 | }, 127 | Unset: []config.Header{ 128 | {Name: "X-Data", When: "$(resp.header::X-Data) == '1'"}, 129 | }, 130 | }, 131 | }, 132 | Nocache: []string{"$(host) == 'localhost'"}, 133 | }, 134 | Cache: testCache, 135 | HTTPScheme: httpScheme, 136 | LogLevel: logLevel, 137 | LogOutput: logOutput, 138 | }, 139 | }, 140 | want: want{ 141 | err: false, 142 | }, 143 | }, 144 | { 145 | name: "ErrorNoBackendAddrs", 146 | args: args{ 147 | cfg: Config{ 148 | FileConfig: config.Proxy{ 149 | Addr: "localhost:9999", 150 | BackendAddrs: []string{}, 151 | Response: config.ProxyResponse{ 152 | Headers: config.ProxyResponseHeaders{ 153 | Set: []config.Header{ 154 | {Name: "X-Kratgo", Value: "true", When: "$(resp.header::X-Data) == '1'"}, 155 | }, 156 | Unset: []config.Header{ 157 | {Name: "X-Data", When: "$(resp.header::X-Data) == '1'"}, 158 | }, 159 | }, 160 | }, 161 | Nocache: []string{"$(host) == 'localhost'"}, 162 | }, 163 | Cache: testCache, 164 | HTTPScheme: httpScheme, 165 | LogLevel: logLevel, 166 | LogOutput: logOutput, 167 | }, 168 | }, 169 | want: want{ 170 | err: true, 171 | }, 172 | }, 173 | { 174 | name: "ErrorParseNoCacheRules", 175 | args: args{ 176 | cfg: Config{ 177 | FileConfig: config.Proxy{ 178 | Addr: "localhost:9999", 179 | BackendAddrs: []string{"localhost:8881", "localhost:8882"}, 180 | Response: config.ProxyResponse{ 181 | Headers: config.ProxyResponseHeaders{ 182 | Set: []config.Header{ 183 | {Name: "X-Kratgo", Value: "true", When: "$(resp.header::X-Data) == '1'"}, 184 | }, 185 | Unset: []config.Header{ 186 | {Name: "X-Data", When: "$(resp.header::X-Data) == '1'"}, 187 | }, 188 | }, 189 | }, 190 | Nocache: []string{"$(fake) == 'localhost'"}, 191 | }, 192 | Cache: testCache, 193 | HTTPScheme: httpScheme, 194 | LogLevel: logLevel, 195 | LogOutput: logOutput, 196 | }, 197 | }, 198 | want: want{ 199 | err: true, 200 | }, 201 | }, 202 | { 203 | name: "ErrorParseHeaderRulesSet", 204 | args: args{ 205 | cfg: Config{ 206 | FileConfig: config.Proxy{ 207 | Addr: "localhost:9999", 208 | BackendAddrs: []string{"localhost:8881", "localhost:8882"}, 209 | Response: config.ProxyResponse{ 210 | Headers: config.ProxyResponseHeaders{ 211 | Set: []config.Header{ 212 | {Name: "X-Kratgo", Value: "true", When: "$(fake::X-Data) == '1'"}, 213 | }, 214 | Unset: []config.Header{ 215 | {Name: "X-Data", When: "$(resp.header::X-Data) == '1'"}, 216 | }, 217 | }, 218 | }, 219 | Nocache: []string{"$(host) == 'localhost'"}, 220 | }, 221 | Cache: testCache, 222 | HTTPScheme: httpScheme, 223 | LogLevel: logLevel, 224 | LogOutput: logOutput, 225 | }, 226 | }, 227 | want: want{ 228 | err: true, 229 | }, 230 | }, 231 | { 232 | name: "ErrorParseHeaderRulesUnset", 233 | args: args{ 234 | cfg: Config{ 235 | FileConfig: config.Proxy{ 236 | Addr: "localhost:9999", 237 | BackendAddrs: []string{"localhost:8881", "localhost:8882"}, 238 | Response: config.ProxyResponse{ 239 | Headers: config.ProxyResponseHeaders{ 240 | Set: []config.Header{ 241 | {Name: "X-Kratgo", Value: "true", When: "$(resp.header::X-Data) == '1'"}, 242 | }, 243 | Unset: []config.Header{ 244 | {Name: "X-Data", When: "$(fake::X-Data) == '1'"}, 245 | }, 246 | }, 247 | }, 248 | Nocache: []string{"$(host) == 'localhost'"}, 249 | }, 250 | Cache: testCache, 251 | HTTPScheme: httpScheme, 252 | LogLevel: logLevel, 253 | LogOutput: logOutput, 254 | }, 255 | }, 256 | want: want{ 257 | err: true, 258 | }, 259 | }, 260 | } 261 | 262 | for _, tt := range tests { 263 | t.Run(tt.name, func(t *testing.T) { 264 | p, err := New(tt.args.cfg) 265 | if (err != nil) != tt.want.err { 266 | t.Fatalf("New() error == '%v', want '%v'", err, tt.want.err) 267 | } 268 | 269 | if tt.want.err { 270 | return 271 | } 272 | 273 | if !reflect.DeepEqual(p.fileConfig, tt.args.cfg.FileConfig) { 274 | t.Errorf("New() fileConfig == '%v', want '%v'", p.fileConfig, tt.args.cfg.FileConfig) 275 | } 276 | 277 | if p.log == nil { 278 | t.Errorf("New() fileConfig is '%v'", nil) 279 | } 280 | 281 | if p.cache == nil { 282 | t.Errorf("New() cache is '%v'", nil) 283 | } 284 | 285 | if p.httpScheme != httpScheme { 286 | t.Errorf("New() httpScheme == '%v', want '%v'", p.httpScheme, httpScheme) 287 | } 288 | 289 | totalBackends := len(tt.args.cfg.FileConfig.BackendAddrs) 290 | if len(p.backends) != len(tt.args.cfg.FileConfig.BackendAddrs) { 291 | t.Errorf("New() backends == '%v', want '%v'", p.backends, tt.args.cfg.FileConfig.BackendAddrs) 292 | } 293 | 294 | if p.totalBackends != totalBackends { 295 | t.Errorf("New() totalBackends == '%v', want '%v'", p.totalBackends, totalBackends) 296 | } 297 | 298 | if p.tools.New == nil { 299 | t.Errorf("New() tools.New is '%v'", nil) 300 | } 301 | }) 302 | } 303 | } 304 | 305 | func TestProxy_getBackend(t *testing.T) { 306 | p, err := New(testConfig()) 307 | if err != nil { 308 | t.Fatal(err) 309 | } 310 | 311 | var prevBackend fetcher 312 | for i := 0; i < len(p.backends)*3; i++ { 313 | backend := p.getBackend() 314 | 315 | if p.totalBackends == 1 { 316 | if prevBackend != nil && backend != prevBackend { 317 | t.Errorf("Proxy.getBackend() returns other backend, current '%p', previous '%p'", backend, prevBackend) 318 | } 319 | } else { 320 | if backend == prevBackend { 321 | t.Errorf("Proxy.getBackend() returns same backend, current '%p', previous '%p'", backend, prevBackend) 322 | } 323 | } 324 | 325 | prevBackend = backend 326 | } 327 | } 328 | 329 | func TestProxy_newEvaluableExpression(t *testing.T) { 330 | type args struct { 331 | rule string 332 | } 333 | 334 | type want struct { 335 | strExpr string 336 | regexExpr *regexp.Regexp 337 | params []ruleParam 338 | err bool 339 | } 340 | 341 | tests := []struct { 342 | name string 343 | args args 344 | want want 345 | }{ 346 | { 347 | name: "method", 348 | args: args{ 349 | rule: fmt.Sprintf("$(method) == '%s'", "GET"), 350 | }, 351 | want: want{ 352 | strExpr: fmt.Sprintf("%s == '%s'", config.EvalMethodVar, "GET"), 353 | params: []ruleParam{{name: config.EvalMethodVar, subKey: ""}}, 354 | err: false, 355 | }, 356 | }, 357 | { 358 | name: "host", 359 | args: args{ 360 | rule: fmt.Sprintf("$(host) == '%s'", "www.kratgo.com"), 361 | }, 362 | want: want{ 363 | strExpr: fmt.Sprintf("%s == '%s'", config.EvalHostVar, "www.kratgo.com"), 364 | params: []ruleParam{{name: config.EvalHostVar, subKey: ""}}, 365 | err: false, 366 | }, 367 | }, 368 | { 369 | name: "path", 370 | args: args{ 371 | rule: fmt.Sprintf("$(path) == '%s'", "/es/"), 372 | }, 373 | want: want{ 374 | strExpr: fmt.Sprintf("%s == '%s'", config.EvalPathVar, "/es/"), 375 | params: []ruleParam{{name: config.EvalPathVar, subKey: ""}}, 376 | err: false, 377 | }, 378 | }, 379 | { 380 | name: "contentType", 381 | args: args{ 382 | rule: fmt.Sprintf("$(contentType) == '%s'", "text/html"), 383 | }, 384 | want: want{ 385 | strExpr: fmt.Sprintf("%s == '%s'", config.EvalContentTypeVar, "text/html"), 386 | params: []ruleParam{{name: config.EvalContentTypeVar, subKey: ""}}, 387 | err: false, 388 | }, 389 | }, 390 | { 391 | name: "statusCode", 392 | args: args{ 393 | rule: fmt.Sprintf("$(statusCode) == '%s'", "200"), 394 | }, 395 | want: want{ 396 | strExpr: fmt.Sprintf("%s == '%s'", config.EvalStatusCodeVar, "200"), 397 | params: []ruleParam{{name: config.EvalStatusCodeVar, subKey: ""}}, 398 | err: false, 399 | }, 400 | }, 401 | { 402 | name: "req.header::", 403 | args: args{ 404 | rule: fmt.Sprintf("$(req.header::X-Data) == '%s'", "Kratgo"), 405 | }, 406 | want: want{ 407 | regexExpr: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2}) == '%s'", config.EvalReqHeaderVar, "Kratgo")), 408 | params: []ruleParam{{name: config.EvalReqHeaderVar, subKey: "X-Data"}}, 409 | err: false, 410 | }, 411 | }, 412 | { 413 | name: "resp.header::", 414 | args: args{ 415 | rule: fmt.Sprintf("$(resp.header::X-Resp-Data) == '%s'", "Kratgo"), 416 | }, 417 | want: want{ 418 | regexExpr: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2}) == '%s'", config.EvalRespHeaderVar, "Kratgo")), 419 | params: []ruleParam{{name: config.EvalRespHeaderVar, subKey: "X-Resp-Data"}}, 420 | err: false, 421 | }, 422 | }, 423 | { 424 | name: "cookie::", 425 | args: args{ 426 | rule: fmt.Sprintf("$(cookie::X-Cookie-Data) == '%s'", "Kratgo"), 427 | }, 428 | want: want{ 429 | regexExpr: regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2}) == '%s'", config.EvalCookieVar, "Kratgo")), 430 | params: []ruleParam{{name: config.EvalCookieVar, subKey: "X-Cookie-Data"}}, 431 | err: false, 432 | }, 433 | }, 434 | { 435 | name: "combo", 436 | args: args{ 437 | rule: fmt.Sprintf("$(path) == '%s' && $(method) != '%s'", "/kratgo", "GET"), 438 | }, 439 | want: want{ 440 | strExpr: fmt.Sprintf("%s == '%s' && %s != '%s'", config.EvalPathVar, "/kratgo", config.EvalMethodVar, "GET"), 441 | params: []ruleParam{ 442 | {name: config.EvalPathVar, subKey: ""}, 443 | {name: config.EvalMethodVar, subKey: ""}, 444 | }, 445 | err: false, 446 | }, 447 | }, 448 | { 449 | name: "Error", 450 | args: args{ 451 | rule: "$(test) /() thod) != asdasd3'", 452 | }, 453 | want: want{ 454 | err: true, 455 | }, 456 | }, 457 | } 458 | 459 | p, err := New(testConfig()) 460 | if err != nil { 461 | t.Fatal(err) 462 | } 463 | 464 | for _, tt := range tests { 465 | t.Run(tt.name, func(t *testing.T) { 466 | expr, params, err := p.newEvaluableExpression(tt.args.rule) 467 | 468 | if (tt.want.err && err == nil) || (!tt.want.err && err != nil) { 469 | t.Fatalf("Proxy.newEvaluableExpression() returns error '%v', want error '%v'", err, tt.want.err) 470 | } 471 | 472 | if !tt.want.err { 473 | strExpr := expr.String() 474 | if tt.want.regexExpr != nil { 475 | if !tt.want.regexExpr.MatchString(strExpr) { 476 | t.Errorf("Proxy.newEvaluableExpression() = '%s', want '%s'", strExpr, tt.want.regexExpr.String()) 477 | } 478 | } else { 479 | if strExpr != tt.want.strExpr { 480 | t.Errorf("Proxy.newEvaluableExpression() = '%s', want '%s'", expr.String(), tt.want.strExpr) 481 | } 482 | } 483 | 484 | for _, ruleParam := range params { 485 | for _, wantParam := range tt.want.params { 486 | if tt.want.regexExpr != nil { 487 | if strings.HasPrefix(ruleParam.name, wantParam.name) && wantParam.subKey == ruleParam.subKey { 488 | goto next 489 | } 490 | } else { 491 | if wantParam.name == ruleParam.name && wantParam.subKey == ruleParam.subKey { 492 | goto next 493 | } 494 | } 495 | } 496 | t.Errorf("Proxy.newEvaluableExpression() unexpected parameter %v", ruleParam) 497 | 498 | next: 499 | } 500 | } 501 | 502 | }) 503 | } 504 | } 505 | 506 | func TestProxy_parseNocacheRules(t *testing.T) { 507 | type args struct { 508 | rules []string 509 | } 510 | 511 | type want struct { 512 | err bool 513 | } 514 | 515 | tests := []struct { 516 | name string 517 | args args 518 | want want 519 | }{ 520 | { 521 | name: "Ok", 522 | args: args{ 523 | rules: []string{ 524 | "$(req.header::X-Requested-With) == 'XMLHttpRequest'", 525 | "$(host) == 'www.kratgo.es' || $(req.header::X-Data) != 'Kratgo'", 526 | }, 527 | }, 528 | want: want{ 529 | err: false, 530 | }, 531 | }, 532 | { 533 | name: "Error", 534 | args: args{ 535 | rules: []string{ 536 | "$(fake::X-Requested-With) == 'XMLHttpRequest'", 537 | }, 538 | }, 539 | want: want{ 540 | err: true, 541 | }, 542 | }, 543 | } 544 | 545 | p, err := New(testConfig()) 546 | if err != nil { 547 | t.Fatal(err) 548 | } 549 | 550 | for _, tt := range tests { 551 | t.Run(tt.name, func(t *testing.T) { 552 | p.fileConfig.Nocache = tt.args.rules 553 | 554 | err := p.parseNocacheRules() 555 | if (err != nil) != tt.want.err { 556 | t.Errorf("Proxy.parseNocacheRules() Unexpected error: %v", err) 557 | } 558 | 559 | if tt.want.err { 560 | return 561 | } 562 | 563 | if len(p.fileConfig.Nocache) != len(p.nocacheRules) { 564 | t.Errorf("Proxy.parseNocacheRules() parsed %d rules, want %d", len(p.fileConfig.Nocache), len(p.nocacheRules)) 565 | } 566 | }) 567 | } 568 | } 569 | 570 | func TestProxy_parseHeadersRules(t *testing.T) { 571 | type args struct { 572 | action typeHeaderAction 573 | rules []config.Header 574 | } 575 | 576 | type want struct { 577 | action typeHeaderAction 578 | err bool 579 | } 580 | 581 | tests := []struct { 582 | name string 583 | args args 584 | want want 585 | }{ 586 | { 587 | name: "Set", 588 | args: args{ 589 | action: setHeaderAction, 590 | rules: []config.Header{ 591 | { 592 | Name: "X-Data", 593 | Value: "Kratgo", 594 | When: "$(path) == '/kratgo'", 595 | }, 596 | { 597 | Name: "X-Data", 598 | Value: "$(req.header::X-Data)", 599 | }, 600 | }, 601 | }, 602 | want: want{ 603 | action: setHeaderAction, 604 | err: false, 605 | }, 606 | }, 607 | { 608 | name: "Unset", 609 | args: args{ 610 | action: unsetHeaderAction, 611 | rules: []config.Header{ 612 | { 613 | Name: "X-Data", 614 | When: "$(path) == '/kratgo'", 615 | }, 616 | { 617 | Name: "X-Data", 618 | }, 619 | }, 620 | }, 621 | want: want{ 622 | action: unsetHeaderAction, 623 | err: false, 624 | }, 625 | }, 626 | { 627 | name: "Error", 628 | args: args{ 629 | action: unsetHeaderAction, 630 | rules: []config.Header{ 631 | { 632 | Name: "X-Data", 633 | When: "$(fake) == /kratgo", 634 | }, 635 | }, 636 | }, 637 | want: want{ 638 | err: true, 639 | }, 640 | }, 641 | } 642 | 643 | p, err := New(testConfig()) 644 | if err != nil { 645 | t.Fatal(err) 646 | } 647 | 648 | for _, tt := range tests { 649 | p.headersRules = p.headersRules[:0] 650 | 651 | t.Run(tt.name, func(t *testing.T) { 652 | err = p.parseHeadersRules(tt.args.action, tt.args.rules) 653 | if (err != nil) != tt.want.err { 654 | t.Fatalf("Proxy.parseHeadersRules() Unexpected error: %v", err) 655 | } 656 | 657 | if tt.want.err { 658 | return 659 | } 660 | 661 | if len(tt.args.rules) != len(p.headersRules) { 662 | t.Errorf("Proxy.parseHeadersRules() parsed %d rules, want %d", len(p.headersRules), len(tt.args.rules)) 663 | } 664 | 665 | for i, pr := range p.headersRules { 666 | if tt.want.action != pr.action { 667 | t.Errorf("Proxy.parseHeadersRules() action == '%d', want '%d'", pr.action, tt.want.action) 668 | } 669 | 670 | configHeader := tt.args.rules[i] 671 | if configHeader.When != "" && pr.expr == nil { 672 | t.Errorf("Proxy.parseHeadersRules() Proxy.headersRules.When '%s' has not be parsed", configHeader.When) 673 | } 674 | 675 | if configHeader.Name != pr.name { 676 | t.Errorf("Proxy.parseHeadersRules() name == '%s', want '%s'", configHeader.Name, pr.name) 677 | } 678 | 679 | _, evalKey, evalSubKey := config.ParseConfigKeys(configHeader.Value) 680 | if evalKey != "" { 681 | if !regexp.MustCompile(fmt.Sprintf("%s([0-9]{1,2})", config.EvalReqHeaderVar)).MatchString(evalKey) { 682 | t.Errorf("Proxy.parseHeadersRules() value.value == '%s', want '%s'", pr.value.value, evalKey) 683 | } 684 | 685 | if evalSubKey != pr.value.subKey { 686 | t.Errorf("Proxy.parseHeadersRules() value.subKey == '%s', want '%s'", pr.value.subKey, evalSubKey) 687 | } 688 | } else { 689 | if configHeader.Value != pr.value.value { 690 | t.Errorf("Proxy.parseHeadersRules() value == '%s', want '%s'", pr.value.value, configHeader.Value) 691 | } 692 | } 693 | } 694 | }) 695 | } 696 | } 697 | 698 | func TestProxy_saveBackendResponse(t *testing.T) { 699 | p, err := New(testConfig()) 700 | if err != nil { 701 | t.Fatal(err) 702 | } 703 | 704 | cacheKey := []byte("test") 705 | path := []byte("/test/") 706 | body := []byte("Test Body") 707 | headers := map[string][]byte{ 708 | "X-Data": []byte("1"), 709 | "X-Data-2": []byte("2"), 710 | "X-Data-3": []byte("3"), 711 | } 712 | entry := cache.AcquireEntry() 713 | 714 | resp := fasthttp.AcquireResponse() 715 | resp.SetBody(body) 716 | for k, v := range headers { 717 | resp.Header.SetCanonical([]byte(k), v) 718 | } 719 | 720 | err = p.saveBackendResponse(cacheKey, path, resp, entry) 721 | if err != nil { 722 | t.Fatalf("Proxy.saveBackendResponse() returns err: %v", err) 723 | } 724 | 725 | entry.Reset() 726 | err = p.cache.GetBytes(cacheKey, entry) 727 | if err != nil { 728 | t.Fatal(err) 729 | } 730 | 731 | r := entry.GetResponse(path) 732 | if r == nil { 733 | t.Fatalf("Proxy.saveBackendResponse() path '%s' not found in cache", path) 734 | } 735 | 736 | if !bytes.Equal(r.Body, body) { 737 | t.Fatalf("Proxy.saveBackendResponse() cache body == '%s', want '%s'", r.Body, body) 738 | } 739 | 740 | for k, v := range headers { 741 | for _, h := range r.Headers { 742 | if string(h.Key) == k && bytes.Equal(h.Value, v) { 743 | goto next 744 | } 745 | } 746 | t.Errorf("Proxy.saveBackendResponse() header '%s=%s' not found in cache", k, v) 747 | 748 | next: 749 | } 750 | } 751 | 752 | func TestProxy_fetchFromBackend(t *testing.T) { 753 | type args struct { 754 | cacheKey []byte 755 | path []byte 756 | body []byte 757 | method []byte 758 | headers map[string][]byte 759 | statusCode int 760 | noCacheRules []string 761 | headersRules []config.Header 762 | 763 | httpClientError error 764 | forceProcessHeaderRulesError bool 765 | forceCheckIfNoCacheError bool 766 | } 767 | 768 | type want struct { 769 | saveInCache bool 770 | err bool 771 | } 772 | 773 | tests := []struct { 774 | name string 775 | args args 776 | want want 777 | }{ 778 | { 779 | name: "StatusOk", 780 | args: args{ 781 | cacheKey: []byte("test"), 782 | path: []byte("/test/"), 783 | body: []byte("Test Body"), 784 | method: []byte("POST"), 785 | headers: map[string][]byte{ 786 | "X-Data": []byte("1"), 787 | "X-Data-2": []byte("2"), 788 | "X-Data-3": []byte("3"), 789 | }, 790 | statusCode: 200, 791 | }, 792 | want: want{ 793 | saveInCache: true, 794 | err: false, 795 | }, 796 | }, 797 | { 798 | name: "StatusRedirect", 799 | args: args{ 800 | cacheKey: []byte("test"), 801 | path: []byte("/test/"), 802 | body: []byte("Test Body"), 803 | method: []byte("GET"), 804 | headers: map[string][]byte{ 805 | headerLocation: []byte("http://www.kratgo.com"), 806 | }, 807 | statusCode: 301, 808 | }, 809 | want: want{ 810 | saveInCache: false, 811 | err: false, 812 | }, 813 | }, 814 | { 815 | name: "NoCacheByRule", 816 | args: args{ 817 | cacheKey: []byte("test"), 818 | path: []byte("/test/"), 819 | body: []byte("Test Body"), 820 | method: []byte("GET"), 821 | headers: map[string][]byte{ 822 | "X-Data": []byte("1"), 823 | }, 824 | statusCode: 200, 825 | noCacheRules: []string{ 826 | "$(path) == '/test/'", 827 | }, 828 | }, 829 | want: want{ 830 | saveInCache: false, 831 | err: false, 832 | }, 833 | }, 834 | { 835 | name: "NoCacheByStatusCode", 836 | args: args{ 837 | cacheKey: []byte("test"), 838 | path: []byte("/test/"), 839 | body: []byte("Test Body"), 840 | method: []byte("GET"), 841 | headers: map[string][]byte{ 842 | "X-Data": []byte("1"), 843 | }, 844 | statusCode: 404, 845 | }, 846 | want: want{ 847 | saveInCache: false, 848 | err: false, 849 | }, 850 | }, 851 | { 852 | name: "ErrorHttpClientDo", 853 | args: args{ 854 | cacheKey: []byte("test"), 855 | path: []byte("/test/"), 856 | body: []byte("Test Body"), 857 | method: []byte("GET"), 858 | headers: map[string][]byte{ 859 | "X-Data": []byte("1"), 860 | }, 861 | statusCode: 404, 862 | httpClientError: errors.New("Error"), 863 | }, 864 | want: want{ 865 | saveInCache: false, 866 | err: true, 867 | }, 868 | }, 869 | { 870 | name: "ErrorParseHeaderRules", 871 | args: args{ 872 | cacheKey: []byte("test"), 873 | path: []byte("/test/"), 874 | body: []byte("Test Body"), 875 | method: []byte("GET"), 876 | headers: map[string][]byte{ 877 | "X-Data": []byte("1"), 878 | }, 879 | statusCode: 404, 880 | headersRules: []config.Header{ 881 | {Name: "X-Data", Value: "1", When: "$(path) == '/kratgo'"}, 882 | }, 883 | forceProcessHeaderRulesError: true, 884 | }, 885 | want: want{ 886 | saveInCache: false, 887 | err: true, 888 | }, 889 | }, 890 | { 891 | name: "ErrorCheckIfNoCache", 892 | args: args{ 893 | cacheKey: []byte("test"), 894 | path: []byte("/test/"), 895 | body: []byte("Test Body"), 896 | method: []byte("GET"), 897 | headers: map[string][]byte{ 898 | "X-Data": []byte("1"), 899 | }, 900 | statusCode: 404, 901 | noCacheRules: []string{ 902 | "$(path) == '/test/'", 903 | }, 904 | forceCheckIfNoCacheError: true, 905 | }, 906 | want: want{ 907 | saveInCache: false, 908 | err: true, 909 | }, 910 | }, 911 | } 912 | 913 | for _, tt := range tests { 914 | t.Run(tt.name, func(t *testing.T) { 915 | cfg := testConfig() 916 | cfg.FileConfig.Nocache = tt.args.noCacheRules 917 | cfg.FileConfig.Response.Headers.Set = tt.args.headersRules 918 | 919 | p, err := New(cfg) 920 | if err != nil { 921 | t.Fatal(err) 922 | } 923 | 924 | if tt.args.forceProcessHeaderRulesError { 925 | p.headersRules[0].params = p.headersRules[0].params[:0] 926 | } 927 | 928 | if tt.args.forceCheckIfNoCacheError { 929 | p.nocacheRules[0].params = p.nocacheRules[0].params[:0] 930 | } 931 | 932 | p.fileConfig.Nocache = tt.args.noCacheRules 933 | p.backends = []fetcher{ 934 | &mockBackend{ 935 | body: tt.args.body, 936 | statusCode: tt.args.statusCode, 937 | headers: tt.args.headers, 938 | err: tt.args.httpClientError, 939 | }, 940 | } 941 | p.totalBackends = len(tt.args.noCacheRules) 942 | 943 | pt := p.acquireTools() 944 | entry := cache.AcquireEntry() 945 | 946 | ctx := new(fasthttp.RequestCtx) 947 | ctx.Request.SetRequestURIBytes(tt.args.path) 948 | ctx.Request.Header.SetMethodBytes(tt.args.method) 949 | for k, v := range tt.args.headers { 950 | ctx.Request.Header.SetCanonical([]byte(k), v) 951 | } 952 | 953 | err = p.fetchFromBackend(tt.args.cacheKey, tt.args.path, ctx, pt) 954 | if (err != nil) != tt.want.err { 955 | t.Errorf("Proxy.fetchFromBackend() Unexpected error: %v", err) 956 | } 957 | 958 | if v := ctx.Request.Header.Peek(proxyReqHeaderKey); string(v) != proxyReqHeaderValue { 959 | t.Errorf("The header '%s = %s' not found in request", proxyReqHeaderKey, proxyReqHeaderValue) 960 | } 961 | 962 | if tt.want.err { 963 | return 964 | } 965 | 966 | err = p.cache.GetBytes(tt.args.cacheKey, entry) 967 | if err != nil { 968 | t.Fatal(err) 969 | } 970 | 971 | if tt.want.saveInCache { 972 | r := entry.GetResponse(tt.args.path) 973 | if r == nil { 974 | t.Fatalf("Proxy.saveBackendResponse() path '%s' not found in cache", tt.args.path) 975 | } 976 | 977 | if !bytes.Equal(r.Body, tt.args.body) { 978 | t.Fatalf("Proxy.saveBackendResponse() cache body == '%s', want '%s'", r.Body, tt.args.body) 979 | } 980 | 981 | for k, v := range tt.args.headers { 982 | for _, h := range r.Headers { 983 | if string(h.Key) == k && bytes.Equal(h.Value, v) { 984 | goto next 985 | } 986 | } 987 | t.Errorf("Proxy.saveBackendResponse() header '%s=%s' not found in cache", k, v) 988 | 989 | next: 990 | } 991 | } 992 | }) 993 | } 994 | } 995 | 996 | func TestProxy_handler(t *testing.T) { 997 | type args struct { 998 | host []byte 999 | path []byte 1000 | headers []cache.ResponseHeader 1001 | cachePath []byte 1002 | noCacheRules []string 1003 | 1004 | forceProcessHeaderRulesError bool 1005 | httpClientError error 1006 | } 1007 | 1008 | type want struct { 1009 | getFromCache bool 1010 | getFromBackend bool 1011 | err bool 1012 | } 1013 | 1014 | tests := []struct { 1015 | name string 1016 | args args 1017 | want want 1018 | }{ 1019 | { 1020 | name: "ResponseFromCache", 1021 | args: args{ 1022 | host: []byte("www.kratgo.com"), 1023 | path: []byte("/test/"), 1024 | headers: []cache.ResponseHeader{ 1025 | {Key: []byte("X-Key"), Value: []byte("1")}, 1026 | }, 1027 | cachePath: []byte("/test/"), 1028 | }, 1029 | want: want{ 1030 | getFromCache: true, 1031 | getFromBackend: false, 1032 | err: false, 1033 | }, 1034 | }, 1035 | { 1036 | name: "ResponseFromCacheNotFound", 1037 | args: args{ 1038 | host: []byte("www.kratgo.com"), 1039 | path: []byte("/test/"), 1040 | cachePath: []byte("/test/data/"), 1041 | }, 1042 | want: want{ 1043 | getFromCache: true, 1044 | getFromBackend: true, 1045 | err: false, 1046 | }, 1047 | }, 1048 | { 1049 | name: "ResponseFromBackend", 1050 | args: args{ 1051 | host: []byte("www.kratgo.com"), 1052 | path: []byte("/test/"), 1053 | cachePath: []byte("/test/data/"), 1054 | }, 1055 | want: want{ 1056 | getFromCache: false, 1057 | getFromBackend: true, 1058 | err: false, 1059 | }, 1060 | }, 1061 | { 1062 | name: "ResponseFromBackendByNocache", 1063 | args: args{ 1064 | host: []byte("www.kratgo.com"), 1065 | noCacheRules: []string{ 1066 | "$(host) == 'www.kratgo.com'", 1067 | }, 1068 | }, 1069 | want: want{ 1070 | getFromCache: false, 1071 | getFromBackend: true, 1072 | err: false, 1073 | }, 1074 | }, 1075 | { 1076 | name: "ErrorCheckIfNoCache", 1077 | args: args{ 1078 | host: []byte("www.kratgo.com"), 1079 | noCacheRules: []string{ 1080 | "$(host) == 'www.kratgo.com'", 1081 | }, 1082 | forceProcessHeaderRulesError: true, 1083 | }, 1084 | want: want{ 1085 | err: true, 1086 | }, 1087 | }, 1088 | { 1089 | name: "ErrorFetchFromBackend", 1090 | args: args{ 1091 | host: []byte("www.kratgo.com"), 1092 | httpClientError: errors.New("Error"), 1093 | }, 1094 | want: want{ 1095 | err: true, 1096 | }, 1097 | }, 1098 | } 1099 | 1100 | for _, tt := range tests { 1101 | t.Run(tt.name, func(t *testing.T) { 1102 | cfg := testConfig() 1103 | cfg.FileConfig.Nocache = tt.args.noCacheRules 1104 | 1105 | p, err := New(cfg) 1106 | if err != nil { 1107 | t.Fatal(err) 1108 | } 1109 | 1110 | if tt.args.forceProcessHeaderRulesError { 1111 | p.nocacheRules[0].params = p.nocacheRules[0].params[:0] 1112 | } 1113 | 1114 | ctx := new(fasthttp.RequestCtx) 1115 | ctx.Request.SetRequestURIBytes(tt.args.path) 1116 | ctx.Request.Header.SetHostBytes(tt.args.host) 1117 | 1118 | entry := cache.AcquireEntry() 1119 | response := cache.AcquireResponse() 1120 | response.Path = tt.args.cachePath 1121 | for _, h := range tt.args.headers { 1122 | response.SetHeader(h.Key, h.Value) 1123 | } 1124 | entry.SetResponse(*response) 1125 | p.cache.SetBytes(tt.args.host, *entry) 1126 | 1127 | httpClientMock := &mockBackend{ 1128 | statusCode: 200, 1129 | err: tt.args.httpClientError, 1130 | } 1131 | p.backends = []fetcher{httpClientMock} 1132 | p.totalBackends = len(p.backends) 1133 | 1134 | p.handler(ctx) 1135 | 1136 | if (ctx.Response.StatusCode() == fasthttp.StatusInternalServerError) != tt.want.err { 1137 | t.Errorf("Proxy.handler() Unexpected error: %s", ctx.Response.Body()) 1138 | } 1139 | 1140 | if tt.want.err { 1141 | return 1142 | } 1143 | 1144 | if tt.want.getFromCache { 1145 | if tt.want.getFromBackend && !httpClientMock.called { 1146 | t.Errorf("Proxy.handler() response from backend '%v', want '%v'", false, true) 1147 | } else if !tt.want.getFromBackend && httpClientMock.called { 1148 | t.Errorf("Proxy.handler() response from cache '%v', want '%v'", true, false) 1149 | } 1150 | 1151 | } else { 1152 | if tt.want.getFromBackend && !httpClientMock.called { 1153 | t.Errorf("Proxy.handler() response from backend '%v', want '%v'", false, true) 1154 | } else if !tt.want.getFromBackend && httpClientMock.called { 1155 | t.Errorf("Proxy.handler() response from backend '%v', want '%v'", false, true) 1156 | } 1157 | } 1158 | 1159 | for _, h := range tt.args.headers { 1160 | if !bytes.Equal(ctx.Response.Header.PeekBytes(h.Key), h.Value) { 1161 | t.Errorf("Proxy.handler() the header '%s = %s' not found in response", h.Key, h.Value) 1162 | } 1163 | } 1164 | }) 1165 | } 1166 | } 1167 | 1168 | func TestProxy_ListenAndServe(t *testing.T) { 1169 | serverMock := new(mockServer) 1170 | addr := "localhost:9999" 1171 | 1172 | p, err := New(testConfig()) 1173 | if err != nil { 1174 | t.Fatal(err) 1175 | } 1176 | p.fileConfig.Addr = addr 1177 | p.server = serverMock 1178 | 1179 | p.ListenAndServe() 1180 | 1181 | serverMock.mu.RLock() 1182 | defer serverMock.mu.RUnlock() 1183 | if !serverMock.listenAndServeCalled { 1184 | t.Error("Proxy.ListenAndServe() invalidator is not start") 1185 | } 1186 | 1187 | if serverMock.addr != addr { 1188 | t.Errorf("Proxy.ListenAndServe() addr == '%s', want '%s'", serverMock.addr, addr) 1189 | } 1190 | 1191 | } 1192 | 1193 | func BenchmarkHandler(b *testing.B) { 1194 | p, err := New(testConfig()) 1195 | if err != nil { 1196 | b.Fatal(err) 1197 | } 1198 | p.backends = []fetcher{ 1199 | &mockBackend{ 1200 | body: []byte("Benchmark Response Body"), 1201 | statusCode: 200, 1202 | headers: map[string][]byte{ 1203 | "X-Data": []byte("Kratgo"), 1204 | }, 1205 | }, 1206 | } 1207 | p.totalBackends = len(p.backends) 1208 | 1209 | ctx := new(fasthttp.RequestCtx) 1210 | ctx.Request.SetRequestURI("/bench") 1211 | ctx.Request.Header.SetMethod("GET") 1212 | 1213 | b.ResetTimer() 1214 | for i := 0; i < b.N; i++ { 1215 | p.handler(ctx) 1216 | } 1217 | } 1218 | 1219 | func BenchmarkHandlerWithoutCache(b *testing.B) { 1220 | path := "/bench" 1221 | cfg := testConfig() 1222 | cfg.FileConfig.Nocache = []string{ 1223 | fmt.Sprintf("$(path) == '%s'", path), 1224 | } 1225 | cfg.FileConfig.Response.Headers.Set = []config.Header{ 1226 | {Name: "X-Data", Value: "1", When: fmt.Sprintf("$(path) == '%s'", path)}, 1227 | } 1228 | 1229 | p, err := New(cfg) 1230 | if err != nil { 1231 | b.Fatal(err) 1232 | } 1233 | 1234 | p.backends = []fetcher{ 1235 | &mockBackend{ 1236 | body: []byte("Benchmark Response Body"), 1237 | statusCode: 200, 1238 | headers: map[string][]byte{ 1239 | "X-Data": []byte("Kratgo"), 1240 | }, 1241 | }, 1242 | } 1243 | p.totalBackends = len(p.backends) 1244 | 1245 | ctx := new(fasthttp.RequestCtx) 1246 | ctx.Request.SetRequestURI(path) 1247 | ctx.Request.Header.SetMethod("GET") 1248 | 1249 | b.ResetTimer() 1250 | for i := 0; i < b.N; i++ { 1251 | p.handler(ctx) 1252 | } 1253 | } 1254 | -------------------------------------------------------------------------------- /modules/proxy/types.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | logger "github.com/savsgio/go-logger/v4" 8 | "github.com/savsgio/govaluate/v3" 9 | "github.com/savsgio/kratgo/modules/cache" 10 | "github.com/savsgio/kratgo/modules/config" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | // Config ... 15 | type Config struct { 16 | FileConfig config.Proxy 17 | Cache *cache.Cache 18 | 19 | HTTPScheme string 20 | 21 | LogLevel logger.Level 22 | LogOutput io.Writer 23 | } 24 | 25 | // Proxy ... 26 | type Proxy struct { 27 | fileConfig config.Proxy 28 | 29 | server server 30 | cache *cache.Cache 31 | 32 | backends []fetcher 33 | totalBackends int 34 | currentBackend int 35 | 36 | httpScheme string 37 | 38 | nocacheRules []rule 39 | headersRules []headerRule 40 | 41 | log *logger.Logger 42 | tools sync.Pool 43 | mu sync.RWMutex 44 | } 45 | 46 | type proxyTools struct { 47 | params *evalParams 48 | entry *cache.Entry 49 | } 50 | 51 | type httpClient struct { 52 | req *fasthttp.Request 53 | resp *fasthttp.Response 54 | 55 | executeHeaderRule bool 56 | } 57 | 58 | type evalParams struct { 59 | p map[string]interface{} 60 | } 61 | 62 | type ruleParam struct { 63 | name string 64 | subKey string 65 | } 66 | 67 | type headerValue struct { 68 | value string 69 | subKey string 70 | } 71 | 72 | type rule struct { 73 | expr *govaluate.EvaluableExpression 74 | params []ruleParam 75 | } 76 | 77 | type typeHeaderAction int 78 | 79 | type headerRule struct { 80 | rule 81 | 82 | action typeHeaderAction 83 | name string 84 | value headerValue 85 | } 86 | 87 | // ###### INTERFACES ###### 88 | 89 | type fetcher interface { 90 | Do(req *fasthttp.Request, resp *fasthttp.Response) error 91 | } 92 | 93 | // Server ... 94 | type server interface { 95 | ListenAndServe(addr string) error 96 | } 97 | -------------------------------------------------------------------------------- /modules/proxy/utils.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | gstrconv "github.com/savsgio/gotils/strconv" 9 | "github.com/savsgio/kratgo/modules/config" 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | // Hop-by-hop headers. These are removed when sent to the backend. 14 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html 15 | var hopHeaders = []string{ 16 | "Connection", 17 | "Keep-Alive", 18 | "Proxy-Authenticate", 19 | "Proxy-Authorization", 20 | "Te", // canonicalized version of "TE" 21 | "Trailers", 22 | "Transfer-Encoding", 23 | "Upgrade", 24 | } 25 | 26 | // TOOLS 27 | 28 | func intSliceIndexOf(vs []int, t int) int { 29 | for i, v := range vs { 30 | if v == t { 31 | return i 32 | } 33 | } 34 | return -1 35 | } 36 | 37 | func intSliceInclude(vs []int, t int) bool { 38 | return intSliceIndexOf(vs, t) >= 0 39 | } 40 | 41 | func stringSliceIndexOf(vs []string, t string) int { 42 | for i, v := range vs { 43 | if v == t { 44 | return i 45 | } 46 | } 47 | return -1 48 | } 49 | 50 | func stringSliceInclude(vs []string, t string) bool { 51 | return stringSliceIndexOf(vs, t) >= 0 52 | } 53 | 54 | // HTTP 55 | 56 | func cloneHeaders(dst, src *fasthttp.RequestHeader) { 57 | src.VisitAll(func(key, value []byte) { 58 | if !stringSliceInclude(hopHeaders, gstrconv.B2S(key)) { 59 | // fmt.Println(gstrconv.B2S(key), gstrconv.B2S(value)) 60 | dst.SetCanonical(key, value) 61 | } 62 | }) 63 | } 64 | 65 | func getEvalValue(ctx *fasthttp.RequestCtx, name, key string) string { 66 | value := name 67 | 68 | switch name { 69 | case config.EvalMethodVar: 70 | value = gstrconv.B2S(ctx.Request.Header.Method()) 71 | 72 | case config.EvalHostVar: 73 | value = gstrconv.B2S(ctx.Request.Host()) 74 | 75 | case config.EvalPathVar: 76 | value = gstrconv.B2S(ctx.Request.URI().PathOriginal()) 77 | 78 | case config.EvalContentTypeVar: 79 | value = gstrconv.B2S(ctx.Response.Header.ContentType()) 80 | 81 | case config.EvalStatusCodeVar: 82 | value = strconv.Itoa(ctx.Response.StatusCode()) 83 | 84 | default: 85 | if strings.HasPrefix(name, config.EvalReqHeaderVar) { 86 | value = gstrconv.B2S(ctx.Request.Header.Peek(key)) 87 | 88 | } else if strings.HasPrefix(name, config.EvalRespHeaderVar) { 89 | value = gstrconv.B2S(ctx.Response.Header.Peek(key)) 90 | 91 | } else if strings.HasPrefix(name, config.EvalCookieVar) { 92 | value = gstrconv.B2S(ctx.Request.Header.Cookie(key)) 93 | } 94 | } 95 | 96 | return value 97 | } 98 | 99 | func checkIfNoCache(ctx *fasthttp.RequestCtx, rules []rule, params *evalParams) (bool, error) { 100 | for _, r := range rules { 101 | params.reset() 102 | 103 | for _, p := range r.params { 104 | params.set(p.name, getEvalValue(ctx, p.name, p.subKey)) 105 | } 106 | 107 | result, err := r.expr.Evaluate(params.all()) 108 | if err != nil { 109 | return false, fmt.Errorf("Invalid nocache rule: %v", err) 110 | } 111 | 112 | if result.(bool) { 113 | return true, nil 114 | } 115 | } 116 | 117 | return false, nil 118 | } 119 | 120 | func processHeaderRules(ctx *fasthttp.RequestCtx, rules []headerRule, params *evalParams) error { 121 | for _, r := range rules { 122 | params.reset() 123 | 124 | executeHeaderRule := true 125 | 126 | if r.expr != nil { 127 | for _, p := range r.params { 128 | params.set(p.name, getEvalValue(ctx, p.name, p.subKey)) 129 | } 130 | 131 | result, err := r.expr.Evaluate(params.all()) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | executeHeaderRule = result.(bool) 137 | } 138 | 139 | if !executeHeaderRule { 140 | continue 141 | } 142 | 143 | if r.action == setHeaderAction { 144 | ctx.Response.Header.Set(r.name, getEvalValue(ctx, r.value.value, r.value.subKey)) 145 | } else { 146 | ctx.Response.Header.Del(r.name) 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /modules/proxy/utils_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/savsgio/kratgo/modules/config" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | func Test_intSliceIndexOf(t *testing.T) { 12 | array := []int{1, 2, 3, 4, 5} 13 | 14 | n := 3 15 | if i := intSliceIndexOf(array, n); i < 0 { 16 | t.Errorf("intSliceIndexOf() = %v, want %v", i, 2) 17 | } 18 | 19 | n = 9 20 | if i := intSliceIndexOf(array, n); i > -1 { 21 | t.Errorf("intSliceIndexOf() = %v, want %v", i, -1) 22 | } 23 | } 24 | 25 | func Test_intSliceInclude(t *testing.T) { 26 | array := []int{1, 2, 3, 4, 5} 27 | 28 | n := 3 29 | if ok := intSliceInclude(array, n); !ok { 30 | t.Errorf("intSliceIndexOf() = %v, want %v", ok, true) 31 | } 32 | 33 | n = 9 34 | if ok := intSliceInclude(array, n); ok { 35 | t.Errorf("intSliceIndexOf() = %v, want %v", ok, false) 36 | } 37 | } 38 | 39 | func Test_stringSliceIndexOf(t *testing.T) { 40 | array := []string{"kratgo", "fast", "http", "cache"} 41 | 42 | s := "fast" 43 | if i := stringSliceIndexOf(array, s); i < 0 { 44 | t.Errorf("stringSliceIndexOf() = %v, want %v", i, 2) 45 | } 46 | 47 | s = "slow" 48 | if i := stringSliceIndexOf(array, s); i > -1 { 49 | t.Errorf("stringSliceIndexOf() = %v, want %v", i, -1) 50 | } 51 | } 52 | 53 | func Test_stringSliceInclude(t *testing.T) { 54 | array := []string{"kratgo", "fast", "http", "cache"} 55 | 56 | s := "fast" 57 | if ok := stringSliceInclude(array, s); !ok { 58 | t.Errorf("stringSliceInclude() = %v, want %v", ok, true) 59 | } 60 | 61 | s = "slow" 62 | if ok := stringSliceInclude(array, s); ok { 63 | t.Errorf("stringSliceInclude() = %v, want %v", ok, false) 64 | } 65 | } 66 | 67 | func Test_cloneHeaders(t *testing.T) { 68 | k1 := "Kratgo" 69 | v1 := "Fast" 70 | 71 | req1 := fasthttp.AcquireRequest() 72 | req2 := fasthttp.AcquireRequest() 73 | 74 | req1.Header.Set(k1, v1) 75 | for i, header := range hopHeaders { 76 | req1.Header.Set(header, strconv.Itoa(i)) 77 | } 78 | 79 | cloneHeaders(&req2.Header, &req1.Header) 80 | 81 | isK1InReq2 := false 82 | req2.Header.VisitAll(func(k, v []byte) { 83 | if stringSliceInclude(hopHeaders, string(k)) { 84 | t.Errorf("cloneHeaders() invalid header '%s'", k) 85 | } 86 | 87 | if string(k) == k1 { 88 | isK1InReq2 = true 89 | 90 | if string(v) != v1 { 91 | t.Errorf("cloneHeaders() invalid header value of '%s' = '%s', want '%s'", k, v, v1) 92 | } 93 | } 94 | }) 95 | 96 | if !isK1InReq2 { 97 | t.Errorf("cloneHeaders() the header '%s' is not cloned", k1) 98 | } 99 | } 100 | 101 | func Test_getEvalValue(t *testing.T) { 102 | ctx := new(fasthttp.RequestCtx) 103 | 104 | method := "POST" 105 | host := "www.kratgo.com" 106 | path := "/data/" 107 | contentType := "application/json" 108 | statusCode := 301 109 | reqHeaderName := "X-Kratgo" 110 | reqHeaderValue := "Fast" 111 | respHeaderName := "X-Data" 112 | respHeaderValue := "false" 113 | cookieName := "kratcookie" 114 | cookieValue := "1234" 115 | 116 | ctx.Request.SetRequestURI(path) 117 | ctx.Request.Header.SetMethod(method) 118 | ctx.Request.Header.SetHost(host) 119 | ctx.Request.Header.Set(reqHeaderName, reqHeaderValue) 120 | ctx.Request.Header.SetCookie(cookieName, cookieValue) 121 | 122 | ctx.Response.Header.SetContentType(contentType) 123 | ctx.Response.Header.Set(respHeaderName, respHeaderValue) 124 | ctx.Response.SetStatusCode(statusCode) 125 | 126 | type args struct { 127 | name string 128 | key string 129 | } 130 | 131 | type want struct { 132 | value string 133 | } 134 | 135 | tests := []struct { 136 | name string 137 | args args 138 | want want 139 | }{ 140 | { 141 | name: "method", 142 | args: args{ 143 | name: config.EvalMethodVar, 144 | }, 145 | want: want{ 146 | value: method, 147 | }, 148 | }, 149 | { 150 | name: "host", 151 | args: args{ 152 | name: config.EvalHostVar, 153 | }, 154 | want: want{ 155 | value: host, 156 | }, 157 | }, 158 | { 159 | name: "path", 160 | args: args{ 161 | name: config.EvalPathVar, 162 | }, 163 | want: want{ 164 | value: path, 165 | }, 166 | }, 167 | { 168 | name: "content-type", 169 | args: args{ 170 | name: config.EvalContentTypeVar, 171 | }, 172 | want: want{ 173 | value: contentType, 174 | }, 175 | }, 176 | { 177 | name: "status-code", 178 | args: args{ 179 | name: config.EvalStatusCodeVar, 180 | }, 181 | want: want{ 182 | value: strconv.Itoa(statusCode), 183 | }, 184 | }, 185 | { 186 | name: "request-header", 187 | args: args{ 188 | name: config.EvalReqHeaderVar, 189 | key: reqHeaderName, 190 | }, 191 | want: want{ 192 | value: reqHeaderValue, 193 | }, 194 | }, 195 | { 196 | name: "response-header", 197 | args: args{ 198 | name: config.EvalRespHeaderVar, 199 | key: respHeaderName, 200 | }, 201 | want: want{ 202 | value: respHeaderValue, 203 | }, 204 | }, 205 | { 206 | name: "cookie", 207 | args: args{ 208 | name: config.EvalCookieVar, 209 | key: cookieName, 210 | }, 211 | want: want{ 212 | value: cookieValue, 213 | }, 214 | }, 215 | { 216 | name: "unknown", 217 | args: args{ 218 | name: "unknown", 219 | }, 220 | want: want{ 221 | value: "unknown", 222 | }, 223 | }, 224 | } 225 | 226 | for _, tt := range tests { 227 | t.Run(tt.name, func(t *testing.T) { 228 | if got := getEvalValue(ctx, tt.args.name, tt.args.key); got != tt.want.value { 229 | t.Errorf("getEvalValue() = '%v', want '%v'", got, tt.want) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | func Test_checkIfNoCache(t *testing.T) { 236 | cfg := testConfig() 237 | cfg.FileConfig.Nocache = []string{ 238 | "$(method) == 'POST' && $(host) != 'www.kratgo.com'", 239 | } 240 | p, _ := New(cfg) 241 | 242 | type args struct { 243 | method string 244 | host string 245 | delRuleParams bool 246 | } 247 | 248 | type want struct { 249 | noCache bool 250 | err bool 251 | } 252 | 253 | tests := []struct { 254 | name string 255 | args args 256 | want want 257 | }{ 258 | { 259 | name: "Yes", 260 | args: args{ 261 | method: "POST", 262 | host: "www.example.com", 263 | }, 264 | want: want{ 265 | noCache: true, 266 | err: false, 267 | }, 268 | }, 269 | { 270 | name: "No1", 271 | args: args{ 272 | method: "GET", 273 | host: "www.kratgo.com", 274 | }, 275 | want: want{ 276 | noCache: false, 277 | err: false, 278 | }, 279 | }, 280 | { 281 | name: "No2", 282 | args: args{ 283 | method: "POST", 284 | host: "www.kratgo.com", 285 | }, 286 | want: want{ 287 | noCache: false, 288 | err: false, 289 | }, 290 | }, 291 | { 292 | name: "No3", 293 | args: args{ 294 | method: "GET", 295 | host: "www.example.com", 296 | }, 297 | want: want{ 298 | noCache: false, 299 | err: false, 300 | }, 301 | }, 302 | { 303 | name: "Error", 304 | args: args{ 305 | method: "GET", 306 | host: "www.example.com", 307 | delRuleParams: true, 308 | }, 309 | want: want{ 310 | noCache: false, 311 | err: true, 312 | }, 313 | }, 314 | } 315 | 316 | for _, tt := range tests { 317 | t.Run(tt.name, func(t *testing.T) { 318 | ctx := new(fasthttp.RequestCtx) 319 | params := acquireEvalParams() 320 | 321 | ctx.Request.Header.SetMethod(tt.args.method) 322 | ctx.Request.Header.SetHost(tt.args.host) 323 | 324 | if tt.args.delRuleParams { 325 | for i := range p.nocacheRules { 326 | r := &p.nocacheRules[i] 327 | r.params = r.params[:0] 328 | } 329 | } 330 | 331 | noCache, err := checkIfNoCache(ctx, p.nocacheRules, params) 332 | if (err != nil) != tt.want.err { 333 | t.Errorf("Unexpected error: %v", err) 334 | } 335 | 336 | if tt.want.err { 337 | return 338 | } 339 | 340 | if noCache != tt.want.noCache { 341 | t.Errorf("checkIfNoCache() = '%v', want '%v'", noCache, tt.want.noCache) 342 | } 343 | }) 344 | } 345 | } 346 | 347 | func TestHTTPClient_processHeaderRules(t *testing.T) { 348 | type args struct { 349 | processWithoutRuleParams bool 350 | } 351 | 352 | type want struct { 353 | err bool 354 | } 355 | 356 | tests := []struct { 357 | name string 358 | args args 359 | want want 360 | }{ 361 | { 362 | name: "Ok", 363 | args: args{ 364 | processWithoutRuleParams: false, 365 | }, 366 | want: want{ 367 | err: false, 368 | }, 369 | }, 370 | { 371 | name: "Error", 372 | args: args{ 373 | processWithoutRuleParams: true, 374 | }, 375 | want: want{ 376 | err: true, 377 | }, 378 | }, 379 | } 380 | 381 | setName1 := "Kratgo" 382 | setValue1 := "Fast" 383 | setWhen1 := "$(resp.header::Content-Type) == 'text/html'" 384 | 385 | setName2 := "X-Data" 386 | setValue2 := "1" 387 | 388 | // This header not fulfill the condition because $(req.header::X-Data) == '123' 389 | setName3 := "X-NotSet" 390 | setValue3 := "yes" 391 | setWhen3 := "$(req.header::X-Data) != '123'" 392 | // ---- 393 | 394 | unsetName1 := "X-Delete" 395 | unsetWhen1 := "$(resp.header::Content-Type) == 'text/html'" 396 | 397 | unsetName2 := "X-MyHeader" 398 | 399 | setHeadersRulesConfig := []config.Header{ 400 | { 401 | Name: setName1, 402 | Value: setValue1, 403 | When: setWhen1, 404 | }, 405 | { 406 | Name: setName2, 407 | Value: setValue2, 408 | }, 409 | { 410 | Name: setName3, 411 | Value: setValue3, 412 | When: setWhen3, 413 | }, 414 | } 415 | unsetHeadersRulesConfig := []config.Header{ 416 | { 417 | Name: unsetName1, 418 | When: unsetWhen1, 419 | }, 420 | { 421 | Name: unsetName2, 422 | }, 423 | } 424 | 425 | cfg := testConfig() 426 | cfg.FileConfig.Response.Headers.Set = setHeadersRulesConfig 427 | cfg.FileConfig.Response.Headers.Unset = unsetHeadersRulesConfig 428 | p, _ := New(cfg) 429 | 430 | for _, tt := range tests { 431 | t.Run(tt.name, func(t *testing.T) { 432 | if tt.args.processWithoutRuleParams { 433 | for i := range p.headersRules { 434 | p.headersRules[i].params = p.headersRules[i].params[:0] 435 | } 436 | } 437 | 438 | params := acquireEvalParams() 439 | 440 | ctx := new(fasthttp.RequestCtx) 441 | 442 | ctx.Request.Header.Set("X-Data", "123") 443 | ctx.Response.Header.Set(unsetName1, "data") 444 | ctx.Response.Header.Set("FakeHeader", "fake data") 445 | ctx.Response.Header.Set("Content-Type", "text/html") 446 | 447 | err := processHeaderRules(ctx, p.headersRules, params) 448 | if (err != nil) != tt.want.err { 449 | t.Errorf("Unexpected error: %v", err) 450 | } 451 | 452 | if tt.want.err { 453 | return 454 | } 455 | 456 | if v := ctx.Response.Header.Peek(setName1); string(v) != setValue1 { 457 | t.Errorf("httpClient.processHeaderRules() not set header '%s' with value '%s', want '%s==%s'", 458 | setName1, setValue1, setName1, v) 459 | } 460 | 461 | if v := ctx.Response.Header.Peek(setName2); string(v) != setValue2 { 462 | t.Errorf("httpClient.processHeaderRules() not set header '%s' with value '%s', want '%s==%s'", 463 | setName2, setValue2, setName2, v) 464 | } 465 | 466 | if v := ctx.Response.Header.Peek(setName3); len(v) > 0 { 467 | t.Errorf("httpClient.processHeaderRules() header '%s' is setted but not fulfill the condition", setName3) 468 | } 469 | 470 | if v := ctx.Response.Header.Peek(unsetName1); len(v) > 0 { 471 | t.Errorf("httpClient.processHeaderRules() not unset header '%s'", unsetName1) 472 | } 473 | 474 | if v := ctx.Response.Header.Peek(unsetName2); len(v) > 0 { 475 | t.Errorf("httpClient.processHeaderRules() not unset header '%s'", unsetName2) 476 | } 477 | }) 478 | } 479 | } 480 | --------------------------------------------------------------------------------