HTTP API: Pass job timeout in HTTP headers 21/182921/6
authorMaciej Wereski <m.wereski@partner.samsung.com>
Fri, 1 Jun 2018 12:49:10 +0000 (14:49 +0200)
committerMaciej Wereski <m.wereski@partner.samsung.com>
Tue, 10 Jul 2018 08:17:22 +0000 (10:17 +0200)
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 <m.wereski@partner.samsung.com>
http/client/client.go
http/client/client_test.go
http/http.go
http/server/api/v1/api.go
http/server/api/v1/handlers_test.go
http/server/api/v1/testdata/req-info-running-GET.json [new file with mode: 0644]

index 55dd690..20c79eb 100644 (file)
@@ -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"))
 }
index e86b0bd..ce003a1 100644 (file)
@@ -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)
 }
index a5d8973..63d0c22 100644 (file)
@@ -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
index 0fa8dd5..22c5ea9 100644 (file)
@@ -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:
index e5746c6..1aad731 100644 (file)
@@ -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 (file)
index 0000000..6dfcfdb
--- /dev/null
@@ -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