diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1cf73ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM golang:1.14-alpine AS builder +WORKDIR /agentx-loadtest-lunchandlearn +COPY . . +WORKDIR /agentx-loadtest-lunchandlearn/cmd/api + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -mod mod + +FROM alpine:latest AS release + +RUN apk add curl + +WORKDIR /go/bin + +COPY --from=builder /go/bin . + +RUN apk add -U tzdata && \ + apk add --update --no-cache ca-certificates + +# Run commands as non-root user +RUN adduser -S summit +USER summit + +EXPOSE 8080 + +CMD "./api" \ No newline at end of file diff --git a/cmd/api/http/handlers.go b/cmd/api/http/handlers.go index ce10b85..96f9ba1 100644 --- a/cmd/api/http/handlers.go +++ b/cmd/api/http/handlers.go @@ -34,6 +34,17 @@ func (s *Server) setLimit(w http.ResponseWriter, r *http.Request) { s.RateLimiter = rl } +func (s *Server) handleClick(w http.ResponseWriter, r *http.Request) { + // Sleep for a random amount of time up to 100 milliseconds. + time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) + + w.WriteHeader(http.StatusNoContent) + + t := r.Context().Value("rate-limit-token").(*Token) + s.RateLimiter.Release(t) + return +} + func (s *Server) getData(w http.ResponseWriter, r *http.Request) { data := []struct { Item1 string @@ -42,7 +53,7 @@ func (s *Server) getData(w http.ResponseWriter, r *http.Request) { }{ { Item1: "books", - Item2: "hotdogs", + Item2: "hot dogs", Item3: "trinkets", }, } @@ -53,8 +64,8 @@ func (s *Server) getData(w http.ResponseWriter, r *http.Request) { return } - // Sleep for a random amount of time up to 5 seconds. - time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) + // Sleep for a random amount of time up to 1 seconds. + time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) w.Write(bytes) diff --git a/cmd/api/http/router.go b/cmd/api/http/router.go index a2190cd..04d2554 100644 --- a/cmd/api/http/router.go +++ b/cmd/api/http/router.go @@ -20,5 +20,12 @@ func (s *Server) BuildRoutes() http.Handler { r.Get("/", s.getData) }) + r.Route("/click", func(r chi.Router) { + r.Use(s.rateLimiter) + r.Use(s.faulty) + + r.Post("/", s.handleClick) + }) + return r } diff --git a/cmd/api/loadtest-api b/cmd/api/loadtest-api new file mode 100755 index 0000000..f583595 Binary files /dev/null and b/cmd/api/loadtest-api differ diff --git a/cmd/api/main.go b/cmd/api/main.go index ee55872..d2322b5 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -12,7 +12,7 @@ import ( ) func main() { - rl, err := http.NewMaxConcurrencyRateLimiter(10) + rl, err := http.NewMaxConcurrencyRateLimiter(50) if err != nil { log.Fatal(err) } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ec90b98 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.1' + +services: + api: + build: . + ports: + - 8080:8080 \ No newline at end of file diff --git a/loadtest_done.js b/loadtest_done.js new file mode 100644 index 0000000..c6cbac9 --- /dev/null +++ b/loadtest_done.js @@ -0,0 +1,68 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; +import { Rate, Trend } from 'k6/metrics'; + +const successRate = new Rate('OK'); +const dataTrend = new Trend('DataTrend'); +const clickTrend = new Trend('ClickTrend'); + +export const options = { + scenarios: { + ramp_to_current: { + executor: 'ramping-arrival-rate', + startRate: 0, + preAllocatedVUs: 200, + maxVUs: 250, + stages: [ + { duration: '4m', target: 50, }, + ], + }, + maintain_current: { + executor: 'constant-arrival-rate', + rate: 50, + timeUnit: '1s', + startTime: '4m', + duration: '1m', + preAllocatedVUs: 200, + maxVUs: 250, + }, + ramp_to_expected: { + executor: 'ramping-arrival-rate', + startRate: 50, + startTime: '5m', + preAllocatedVUs: 600, + maxVUs: 750, + stages: [ + { duration: '4m', target: 150, }, + ], + }, + maintain_future: { + executor: 'constant-arrival-rate', + rate: 150, + timeUnit: '1s', + startTime: '9m', + duration: '1m', + preAllocatedVUs: 600, + maxVUs: 750, + }, + }, + thresholds: { + ClickTrend: ['p(90)<100','p(95)<200'], + DataTrend: ['p(90)<1000','p(95)<1500'], + OK: ['rate==1'], + }, +}; + +/** + * This is the test case that should be executed. + * + * @param {any} data the data returned by setup + */ +export default function makeAPIRequest(data) { + const dataRes = http.get('http://localhost:8080/data'); + const clickRes = http.post('http://localhost:8080/click'); + + successRate.add(dataRes.status < 300 || clickRes.status < 300); + dataTrend.add(dataRes.timings.duration); + clickTrend.add(clickRes.timings.duration); +} diff --git a/loadtest_template.js b/loadtest_template.js new file mode 100644 index 0000000..857ada1 --- /dev/null +++ b/loadtest_template.js @@ -0,0 +1,27 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; +import { Rate, Counter, Gauge, Trend } from 'k6/metrics'; + +const successRate = new Rate('success_rate'); +const failureCount = new Counter('failures'); +const lastStatusCode = new Gauge('status_code'); +const responseTime = new Trend('response_time', true); + +export const options = { + scenarios: { + ramp_to_current: {}, + maintain_current: {}, + ramp_to_expected: {}, + maintain_expected: {}, + }, + thresholds: {}, +}; + +/** + * This is the test case that should be executed. + * + * @param {any} data the data returned by setup + */ +export default function makeAPIRequest(data) { + +}