diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3085cba..cd99679 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -25,11 +25,8 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v4 - - name: Vet - run: go vet ./... - - name: Build - run: go build -v . + run: make build - name: Test - run: go test -v -cover ./... + run: make test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b35a8b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +fmt: + go fmt ./... + +vet: + go vet ./... +test: + go test -v -cover ./... + +build: + go build -v . + +client: + @go build ./tools/client.go + +.PHONY: client diff --git a/README.md b/README.md index 51e56d6..60fb10d 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,29 @@ _Success code_: ```200``` _Error code_: ```400```, empty key is provided. _Note_: TTL is reset for any subsequent requests for the same key. +```sh +# setting new/updating existing key +curl -X POST http://localhost:8080/key/foo -H 'Content-Type: application/x-www-form-urlencoded' -d 'value=bar' +``` + ## Getting value by its key _HTTP method_: ```GET``` _Request's parameter name_: no parameter is needed. _Success code_: ```200```, response's body contains string value for the key. _Error code_: ```404```, key is not found in the storage. +```sh +# getting value for existing key +curl http://localhost:8080/key/foo +bar +``` + +```sh +# an attempt to get value for unknown key +curl http://localhost:8080/key/xyz +404 There is no record in the storage for key 'xyz'. +``` + ## Deleting value by its key _HTTP method_: ```POST``` _Request's parameter name_: no parameter is needed. @@ -55,6 +72,17 @@ _Error code_: ```404```, key is not found in the storage. When error is occured code ```400``` is returned by server. +```sh +# deleting existing key +curl -X POST http://localhost:8080/key/foo +``` + +```sh +# an attempt to delete unknown key +curl -X POST http://localhost:8080/key/xyz +404 There is no record in the storage for key '%v'. +``` + # Tests Run ```go test -v -cover -count=1 ./...```. diff --git a/router/router.go b/router/router.go index 5239333..cbe6a72 100644 --- a/router/router.go +++ b/router/router.go @@ -27,13 +27,6 @@ const ( firstPart = "key" ) -var httpStatusCodeMessages = map[int]string{ - 200: "", - 400: "400 Malformed request.\n", - 404: "404 There is no record in the storage for key '%v'.\n", - 500: "500 Internal storage error.\n", -} - // GetURLrouter - возвращает функцию маршрутизатор HTTP запросов в зависимости от типа. func GetURLrouter(stor readerWriter) func( w http.ResponseWriter, r *http.Request, @@ -42,14 +35,14 @@ func GetURLrouter(stor readerWriter) func( return func(w http.ResponseWriter, r *http.Request) { keyName, ok := getKeyFromURL(r.URL.Path) if !ok { - w.WriteHeader(400) // Bad request - fmt.Fprint(w, httpStatusCodeMessages[400]) + w.WriteHeader(http.StatusBadRequest) // Bad request + fmt.Fprint(w, getMessage(http.StatusBadRequest, "")) return } reqHandler, isHandlerExists := requestFactory(r.Method) if !isHandlerExists { - w.WriteHeader(400) // Bad request - fmt.Fprint(w, httpStatusCodeMessages[400]) + w.WriteHeader(http.StatusBadRequest) // Bad request + fmt.Fprint(w, getMessage(http.StatusBadRequest, "")) return } val, code := reqHandler(stor, keyName, r) @@ -88,12 +81,12 @@ func methodGET(stor readerWriter, key string, r *http.Request) (string, int) { // get the value by its key val, err := stor.Get(key) if err != nil { - return httpStatusCodeMessages[500], 500 + return getMessage(http.StatusInternalServerError, key), http.StatusInternalServerError } if val == nil { // either error occurred or key is not found in the storage (code 404) - code = 404 - val = []byte(fmt.Sprintf(httpStatusCodeMessages[code], key)) + code = http.StatusNotFound + val = []byte(getMessage(http.StatusNotFound, key)) } return string(val), code } @@ -103,7 +96,7 @@ func methodPOST(stor readerWriter, key string, r *http.Request) (string, int) { value := r.PostFormValue(valueFormFieldName) postProcessingMethod := postMethodFactory(len(r.Form)) httpCode := postProcessingMethod(stor, key, value) - return httpStatusCodeMessages[httpCode], httpCode + return getMessage(httpCode, key), httpCode } func postMethodFactory(formLen int) func(storage readerWriter, key, value string) int { @@ -121,14 +114,14 @@ func deleteElementRequest(storage readerWriter, key, value string) int { delStatus, err := storage.Delete(key) if err != nil { // something went wrong with the storage - return 500 + return http.StatusInternalServerError } if delStatus { // element deleted successfully - return 200 + return http.StatusOK } // element was not found and is not deleted - return 404 + return http.StatusNotFound } func setElementRequest(storage readerWriter, key, value string) int { @@ -136,10 +129,25 @@ func setElementRequest(storage readerWriter, key, value string) int { err := storage.Set(key, value) if err != nil { // something went wrong with the storage - return 500 + return http.StatusInternalServerError } if value != "" { - return 200 + return http.StatusOK + } + return http.StatusBadRequest +} + +func getMessage(code int, key string) string { + switch code { + case http.StatusOK: + return "" + case http.StatusBadRequest: + return "Malformed request" + case http.StatusNotFound: + return fmt.Sprintf("There is no record in the storage for key '%v'", key) + case http.StatusInternalServerError: + return "Internal storage error" + default: + return "" } - return 400 } diff --git a/router/router_test.go b/router/router_test.go index b06448f..68b1cde 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -113,6 +113,7 @@ func Test_getKeyFromURL(t *testing.T) { type myResponseWriter struct { code int + msg []byte } func (resp *myResponseWriter) Header() http.Header { @@ -125,8 +126,8 @@ func (resp *myResponseWriter) WriteHeader(code int) { } func (resp *myResponseWriter) Write(inp []byte) (int, error) { - var er error - return int(1), er + resp.msg = inp + return len(inp), nil } func (resp *myResponseWriter) getCode() int { @@ -156,10 +157,11 @@ func Test_closure(t *testing.T) { r *http.Request } tests := []struct { - name string - args args - want int // код ошибки - text string + name string + args args + want int // код ошибки + errMsg string // error text + text string }{ { name: "Incorrect HTTP method (verb)", @@ -174,7 +176,8 @@ func Test_closure(t *testing.T) { }, }, }, - want: 400, + want: 400, + errMsg: "Malformed request", }, { name: "Getting value from the empty storage", @@ -189,7 +192,8 @@ func Test_closure(t *testing.T) { }, }, }, - want: 404, + want: 404, + errMsg: "There is no record in the storage for key 'key1'", }, { name: "Deleting from the empty storage", @@ -204,7 +208,8 @@ func Test_closure(t *testing.T) { }, }, }, - want: 404, + want: 404, + errMsg: "There is no record in the storage for key '" + correctKey + "'", }, { name: "Setting value with empty key", @@ -219,7 +224,8 @@ func Test_closure(t *testing.T) { }, }, }, - want: 400, + want: 400, + errMsg: "Malformed request", }, { name: "URL keyword is incorrect", @@ -234,7 +240,8 @@ func Test_closure(t *testing.T) { }, }, }, - want: 400, + want: 400, + errMsg: "Malformed request", }, { name: "Correct setting the value by its key", @@ -265,7 +272,8 @@ func Test_closure(t *testing.T) { w: writer, r: reqBad, }, - want: 400, + want: 400, + errMsg: "Malformed request", }, } for _, tt := range tests { @@ -275,8 +283,9 @@ func Test_closure(t *testing.T) { handler(tt.args.w, tt.args.r) got := writer.getCode() - if got != tt.want { - t.Errorf("urlHandler() got = %v, want %v", got, tt.want) + msg := writer.msg + if got != tt.want || string(msg) != tt.errMsg { + t.Errorf("GetURLrouter() got = %v, want %v", got, tt.want) } }) } diff --git a/tools/client.go b/tools/client.go new file mode 100644 index 0000000..93bd926 --- /dev/null +++ b/tools/client.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "math/rand" + "net/http" + "net/http/httptrace" + "net/url" + "sync" + "time" +) + +const ( + baseURL = "http://localhost:8080/key" + defaultNumWorkers = 3 + defaultTotalReqs = 1000 +) + +func main() { + numWorkers := flag.Int("workers", defaultNumWorkers, "Number of workers (concurrency) running in parallel") + reqsPerWorker := flag.Int("reqs", defaultTotalReqs, "Requests per worker") + flag.Parse() + + var wg sync.WaitGroup + wg.Add(*numWorkers) + fmt.Printf("Running with concurrency=%v\n", *numWorkers) + t0 := time.Now() + for i := 0; i < *numWorkers; i++ { + go worker(i, *reqsPerWorker, &wg) + } + + wg.Wait() + t1 := time.Since(t0) + fmt.Println("All workers completed") + totalReqs := (*numWorkers) * (*reqsPerWorker) + // t1 is of type time.Duration which is int64 and represents nanoseconds + rps := float32(totalReqs) / (float32(t1) / 1e9) + fmt.Printf("Requests=%v, elapsed=%v, %v rps\n", totalReqs, t1, rps) +} + +func worker(id, numReqs int, wg *sync.WaitGroup) { + defer wg.Done() + + client := &http.Client{Timeout: 10 * time.Second} + fmt.Printf("Starting worker %v with %v requests ...\n", id, numReqs) + for i := 0; i < numReqs; i++ { + key := fmt.Sprintf("key_%d_%d", id, rand.Intn(1000)) + value := fmt.Sprintf("%d", time.Now().Unix()) + + // Store/update value (form data) + _, err := storeValue(client, key, value) + if err != nil { + fmt.Printf("Worker %d: error storing key %s: %v\n", id, key, err) + continue + } + + // Get value + if err := getValue(client, key, value); err != nil { + fmt.Printf("Worker %d: error getting key %s: %v\n", id, key, err) + } + + // Random delete (10% chance) + if rand.Intn(10) == 0 { + if err := deleteValue(client, key); err != nil { + fmt.Printf("Worker %d: error deleting key %s: %v\n", id, key, err) + } + } + + if i%100 == 0 { + fmt.Printf("Worker %d: completed %d requests\n", id, i) + } + } +} + +func storeValue(client *http.Client, key, value string) (time.Duration, error) { + api_url := baseURL + "/" + key + formData := url.Values{} + formData.Set("value", value) + + var t0 time.Time + trace := &httptrace.ClientTrace{ + DNSStart: func(_ httptrace.DNSStartInfo) { t0 = time.Now() }, + } + + req, _ := http.NewRequest(http.MethodPost, api_url, bytes.NewBufferString(formData.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + resp, err := client.Do(req) + if err != nil { + return time.Duration(0), err + } + resp.Body.Close() + diff := time.Since(t0) + + if resp.StatusCode != http.StatusOK { + return time.Duration(0), fmt.Errorf("status %d", resp.StatusCode) + } + + return diff, nil +} + +func getValue(client *http.Client, key, refValue string) error { + resp, err := client.Get(baseURL + "/" + key) + if err != nil { + return err + } + defer resp.Body.Close() + value, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if string(value) != refValue { + return fmt.Errorf("value incorrect, expected=%v, got=%v", refValue, value) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status %d", resp.StatusCode) + } + return nil +} + +func deleteValue(client *http.Client, key string) error { + req, err := http.NewRequest("POST", baseURL+"/"+key, nil) + if err != nil { + return err + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status %d", resp.StatusCode) + } + return nil +}