From 78b25025284b815bcf8b96197ec461a4fd670abb Mon Sep 17 00:00:00 2001 From: Maciej Wereski Date: Fri, 1 Jun 2018 14:49:10 +0200 Subject: [PATCH] HTTP API: Pass job timeout in HTTP headers Users may want to check timeout value when request gets its resources or access to worker is prolonged. Currently it can be only done with GetRequestInfo() which returns all information about given request. To make this action more convenient HTTP API server will set Boruta-Job-Timeout header when request is in "IN PROGRESS" state. On the client side GetJobTimeout is added. It should be also faster way to obtain timeout value as HEAD method is used instead of GET and there's no JSON parsing. Change-Id: I9533f64be063e97b09c5e378a87968395d6b4072 Signed-off-by: Maciej Wereski --- http/client/client.go | 40 ++++-- http/client/client_test.go | 145 ++++++++++++++++++++- http/http.go | 4 + http/server/api/v1/api.go | 4 + http/server/api/v1/handlers_test.go | 30 ++++- .../api/v1/testdata/req-info-running-GET.json | 1 + 6 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 http/server/api/v1/testdata/req-info-running-GET.json diff --git a/http/client/client.go b/http/client/client.go index 55dd690..20c79eb 100644 --- a/http/client/client.go +++ b/http/client/client.go @@ -154,6 +154,19 @@ func checkStatus(shouldBe int, resp *http.Response) (err error) { return } +// getHeaders is a helper function that makes HEAD HTTP request for given address, +// checks Status and returns HTTP headers and error. +func getHeaders(url string) (http.Header, error) { + resp, err := http.Head(url) + if err != nil { + return nil, err + } + if err = checkStatus(http.StatusNoContent, resp); err != nil { + return nil, err + } + return resp.Header, nil +} + // NewRequest creates new Boruta request. func (client *BorutaClient) NewRequest(caps boruta.Capabilities, priority boruta.Priority, owner boruta.UserInfo, validAfter time.Time, @@ -368,25 +381,34 @@ func (client *BorutaClient) Deregister(uuid boruta.WorkerUUID) error { // request state. func (client *BorutaClient) GetRequestState(reqID boruta.ReqID) (boruta.ReqState, error) { path := client.url + "reqs/" + strconv.Itoa(int(reqID)) - resp, err := http.Head(path) + headers, err := getHeaders(path) if err != nil { return boruta.FAILED, err } - if err = checkStatus(http.StatusNoContent, resp); err != nil { - return boruta.FAILED, err - } - return boruta.ReqState(resp.Header.Get("Boruta-Request-State")), nil + return boruta.ReqState(headers.Get("Boruta-Request-State")), nil } // GetWorkerState is convenient way to check state of a worker with given UUID. func (client *BorutaClient) GetWorkerState(uuid boruta.WorkerUUID) (boruta.WorkerState, error) { path := client.url + "workers/" + string(uuid) - resp, err := http.Head(path) + headers, err := getHeaders(path) if err != nil { return boruta.FAIL, err } - if err = checkStatus(http.StatusNoContent, resp); err != nil { - return boruta.FAIL, err + return boruta.WorkerState(headers.Get("Boruta-Worker-State")), nil +} + +// GetJobTimeout is convenient way to check when Job of a request with given +// reqID will timeout. The request must be in INPROGRESS state. +func (client *BorutaClient) GetJobTimeout(reqID boruta.ReqID) (time.Time, error) { + var t time.Time + path := client.url + "reqs/" + strconv.Itoa(int(reqID)) + headers, err := getHeaders(path) + if err != nil { + return t, err + } + if boruta.ReqState(headers.Get("Boruta-Request-State")) != boruta.INPROGRESS { + return t, errors.New(`request must be in "IN PROGRESS" state`) } - return boruta.WorkerState(resp.Header.Get("Boruta-Worker-State")), nil + return time.Parse(util.DateFormat, headers.Get("Boruta-Job-Timeout")) } diff --git a/http/client/client_test.go b/http/client/client_test.go index e86b0bd..ce003a1 100644 --- a/http/client/client_test.go +++ b/http/client/client_test.go @@ -365,6 +365,73 @@ func TestCheckStatus(t *testing.T) { assert.Equal(err, checkStatus(http.StatusBadRequest, &resp)) } +func TestGetHeaders(t *testing.T) { + prefix := "boruta-headers-" + pathW := "/api/v1/workers/" + pathR := "/api/v1/reqs/" + date := time.Now().Format(util.DateFormat) + + worker := make(http.Header) + worker.Set("Boruta-Worker-State", string(RUN)) + + request := make(http.Header) + request.Set("Boruta-Request-State", string(INPROGRESS)) + request.Set("Boruta-Job-Timeout", date) + + tests := []*testCase{ + &testCase{ + // valid worker + name: prefix + "worker", + path: pathW + validUUID, + status: http.StatusNoContent, + header: worker, + }, + &testCase{ + // valid request + name: prefix + "request", + path: pathR + "1", + status: http.StatusNoContent, + header: request, + }, + + &testCase{ + // invalid UUID + name: prefix + "bad-uuid", + path: pathW + invalidID, + contentType: contentJSON, + status: http.StatusBadRequest, + }, + } + + assert := assert.New(t) + srv := prepareServer(http.MethodHead, tests) + url := srv.URL + defer srv.Close() + + // valid worker + headers, err := getHeaders(url + pathW + validUUID) + assert.Nil(err) + assert.Equal(worker.Get("Boruta-Worker-State"), headers.Get("Boruta-Worker-State")) + + // valid request + headers, err = getHeaders(url + pathR + "1") + assert.Nil(err) + assert.Equal(request.Get("Boruta-Request-State"), headers.Get("Boruta-Request-State")) + assert.Equal(request.Get("Boruta-Job-Timeout"), headers.Get("Boruta-Job-Timeout")) + + // invalid UUID + headers, err = getHeaders(url + pathW + invalidID) + assert.Nil(headers) + assert.Equal(errors.New("bad HTTP status: 400 Bad Request"), err) + + // http.Head failure + url = "http://nosuchaddress.fail" + headers, err = getHeaders(url) + assert.Nil(headers) + assert.NotNil(err) + +} + func TestNewRequest(t *testing.T) { prefix := "new-req-" path := "/api/v1/reqs/" @@ -1043,10 +1110,6 @@ func TestGetRequestState(t *testing.T) { // missing request state, err = client.GetRequestState(ReqID(2)) assert.Equal(errors.New("bad HTTP status: 404 Not Found"), err) - - // http.Head failure - client.url = "http://nosuchaddress.fail" - assert.NotNil(client.GetRequestState(ReqID(1))) } func TestGetWorkerState(t *testing.T) { @@ -1085,8 +1148,76 @@ func TestGetWorkerState(t *testing.T) { // invalid UUID state, err = client.GetWorkerState(invalidID) assert.Equal(errors.New("bad HTTP status: 400 Bad Request"), err) +} - // http.Head failure - client.url = "http://nosuchaddress.fail" - assert.NotNil(client.GetWorkerState(validUUID)) +func TestGetJobTimeout(t *testing.T) { + prefix := "get-job-timeout-" + path := "/api/v1/reqs/" + date := time.Now().Round(time.Second) + + header := make(http.Header) + header.Set("Boruta-Request-State", string(INPROGRESS)) + header.Set("Boruta-Job-Timeout", date.Format(util.DateFormat)) + + wait := make(http.Header) + wait.Set("Boruta-Request-State", string(WAIT)) + + bad := make(http.Header) + bad.Set("Boruta-Request-State", string(INPROGRESS)) + bad.Set("Boruta-Job-Timeout", "fail") + + tests := []*testCase{ + &testCase{ + // valid request + name: prefix + "valid", + path: path + "1", + status: http.StatusNoContent, + header: header, + }, + &testCase{ + // request in wrong state + name: prefix + "wrong-state", + path: path + "2", + status: http.StatusNoContent, + header: wait, + }, + &testCase{ + // missing request + name: prefix + "missing", + path: path + "3", + contentType: contentJSON, + status: http.StatusNotFound, + }, + &testCase{ + // invalid date format + name: prefix + "bad-date", + path: path + "4", + contentType: contentJSON, + status: http.StatusNoContent, + header: bad, + }, + } + + srv := prepareServer(http.MethodHead, tests) + defer srv.Close() + assert, client := initTest(t, srv.URL) + + // valid + timeout, err := client.GetJobTimeout(ReqID(1)) + assert.Nil(err) + assert.True(date.Equal(timeout)) + + // wrong state + _, err = client.GetJobTimeout(ReqID(2)) + assert.Equal(errors.New(`request must be in "IN PROGRESS" state`), err) + + // missing + _, err = client.GetJobTimeout(ReqID(3)) + assert.Equal(errors.New("bad HTTP status: 404 Not Found"), err) + + // wrong date format + _, err = client.GetJobTimeout(ReqID(4)) + assert.NotNil(err) + var parseErr *time.ParseError + assert.IsType(parseErr, err) } diff --git a/http/http.go b/http/http.go index a5d8973..63d0c22 100644 --- a/http/http.go +++ b/http/http.go @@ -19,10 +19,14 @@ package http import ( "net" + "time" . "git.tizen.org/tools/boruta" ) +// DateFormat denotes layout of timestamps used by Boruta HTTP API. +const DateFormat = time.RFC3339 + // ReqIDPack is used for JSON (un)marshaller. type ReqIDPack struct { ReqID diff --git a/http/server/api/v1/api.go b/http/server/api/v1/api.go index 0fa8dd5..22c5ea9 100644 --- a/http/server/api/v1/api.go +++ b/http/server/api/v1/api.go @@ -79,6 +79,10 @@ func routerSetHandler(grp *httptreemux.Group, path string, fn reqHandler, } case ReqInfo: w.Header().Add("Boruta-Request-State", string(data.State)) + if data.State == INPROGRESS { + w.Header().Add("Boruta-Job-Timeout", + data.Job.Timeout.Format(util.DateFormat)) + } case []ReqInfo: w.Header().Add("Boruta-Request-Count", strconv.Itoa(len(data))) case WorkerInfo: diff --git a/http/server/api/v1/handlers_test.go b/http/server/api/v1/handlers_test.go index e5746c6..1aad731 100644 --- a/http/server/api/v1/handlers_test.go +++ b/http/server/api/v1/handlers_test.go @@ -197,10 +197,26 @@ func TestGetRequestInfoHandler(t *testing.T) { header := make(http.Header) header.Set("Boruta-Request-State", "WAITING") - notFoundTest := testFromTempl(notFoundTestTempl, prefix, path+"2", methods...) + timeout, err := time.Parse(dateLayout, future) + assert.Nil(err) + var running ReqInfo + err = json.Unmarshal([]byte(validReqJSON), &running) + assert.Nil(err) + running.ID = ReqID(2) + running.State = INPROGRESS + running.Job = &JobInfo{ + WorkerUUID: validUUID, + Timeout: timeout, + } + rheader := make(http.Header) + rheader.Set("Boruta-Request-State", string(INPROGRESS)) + rheader.Set("Boruta-Job-Timeout", timeout.Format(util.DateFormat)) + + notFoundTest := testFromTempl(notFoundTestTempl, prefix, path+"3", methods...) invalidIDTest := testFromTempl(invalidIDTestTempl, prefix, path+invalidID, methods...) m.rq.EXPECT().GetRequestInfo(ReqID(1)).Return(req, nil).Times(2) - m.rq.EXPECT().GetRequestInfo(ReqID(2)).Return(ReqInfo{}, NotFoundError("Request")).Times(2) + m.rq.EXPECT().GetRequestInfo(ReqID(2)).Return(running, nil).Times(2) + m.rq.EXPECT().GetRequestInfo(ReqID(3)).Return(ReqInfo{}, NotFoundError("Request")).Times(2) tests := []requestTest{ // Get information of existing request. @@ -213,6 +229,16 @@ func TestGetRequestInfoHandler(t *testing.T) { status: http.StatusOK, header: header, }, + // Get information of running request. + { + name: "req-info-running", + path: path + "2", + methods: methods, + json: ``, + contentType: contentTypeJSON, + status: http.StatusOK, + header: rheader, + }, // Try to get request information of request that doesn't exist. notFoundTest, // Try to get request with invalid ID. diff --git a/http/server/api/v1/testdata/req-info-running-GET.json b/http/server/api/v1/testdata/req-info-running-GET.json new file mode 100644 index 0000000..6dfcfdb --- /dev/null +++ b/http/server/api/v1/testdata/req-info-running-GET.json @@ -0,0 +1 @@ +{"ID":2,"Priority":8,"Owner":{"Groups":null},"Deadline":"2200-12-31T01:02:03Z","ValidAfter":"2100-01-01T04:05:06Z","State":"IN PROGRESS","Job":{"WorkerUUID":"ec4898ac-0853-407c-8501-cbb24ef6bd77","Timeout":"2222-12-31T00:00:00Z"},"Caps":{"architecture":"armv7l","monitor":"yes"}} \ No newline at end of file -- 2.7.4