Skip to content

Commit fe8ecf0

Browse files
authored
Merge pull request #20 from EmmEff/implement-build-cancellation
Implement Cancel() API for cancelling builds
2 parents 0657e94 + 7a22b94 commit fe8ecf0

File tree

7 files changed

+90
-19
lines changed

7 files changed

+90
-19
lines changed

client/build.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"bytes"
1010
"context"
1111
"encoding/json"
12+
"fmt"
1213
"net/http"
1314

1415
jsonresp "github.com/sylabs/json-resp"
@@ -22,7 +23,7 @@ func (c *Client) Submit(ctx context.Context, br BuildRequest) (bi BuildInfo, err
2223
return
2324
}
2425

25-
req, err := c.newRequest(http.MethodPost, "/v1/build", "", bytes.NewReader(b))
26+
req, err := c.newRequest(http.MethodPost, "/v1/build", bytes.NewReader(b))
2627
if err != nil {
2728
return
2829
}
@@ -42,3 +43,23 @@ func (c *Client) Submit(ctx context.Context, br BuildRequest) (bi BuildInfo, err
4243
}
4344
return
4445
}
46+
47+
// Cancel cancels an existing build. The context controls the lifetime of the
48+
// request.
49+
func (c *Client) Cancel(ctx context.Context, buildID string) error {
50+
req, err := c.newRequest(http.MethodPut, fmt.Sprintf("/v1/build/%s/_cancel", buildID), nil)
51+
if err != nil {
52+
return err
53+
}
54+
c.Logger.Logf("Sending build cancellation request to %s", req.URL.String())
55+
56+
res, err := c.HTTPClient.Do(req.WithContext(ctx))
57+
if err != nil {
58+
return err
59+
}
60+
defer res.Body.Close()
61+
if res.StatusCode != http.StatusNoContent {
62+
return fmt.Errorf("build cancellation failed: http status %d", res.StatusCode)
63+
}
64+
return nil
65+
}

client/build_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,29 @@ func TestSubmit(t *testing.T) {
8787
})
8888
}
8989
}
90+
91+
func TestCancel(t *testing.T) {
92+
// Start a mock server
93+
m := mockService{t: t}
94+
s := httptest.NewServer(&m)
95+
defer s.Close()
96+
97+
// Enough of a struct to test with
98+
url, err := url.Parse(s.URL)
99+
if err != nil {
100+
t.Fatalf("failed to parse URL: %v", err)
101+
}
102+
c, err := client.New(&client.Config{
103+
BaseURL: url.String(),
104+
})
105+
if err != nil {
106+
t.Fatalf("failed to parse URL: %v", err)
107+
}
108+
109+
m.cancelResponseCode = 204
110+
111+
err = c.Cancel(context.Background(), "00000000")
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
}

client/client.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,9 @@ func New(cfg *Config) (c *Client, err error) {
8888
}
8989

9090
// newRequest returns a new Request given a method, path, query, and optional body.
91-
func (c *Client) newRequest(method, path, rawQuery string, body io.Reader) (r *http.Request, err error) {
91+
func (c *Client) newRequest(method, path string, body io.Reader) (r *http.Request, err error) {
9292
u := c.BaseURL.ResolveReference(&url.URL{
93-
Path: path,
94-
RawQuery: rawQuery,
93+
Path: path,
9594
})
9695

9796
r, err = http.NewRequest(method, u.String(), body)

client/client_test.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type mockService struct {
2929
wsCloseCode int
3030
statusResponseCode int
3131
imageResponseCode int
32+
cancelResponseCode int
3233
httpAddr string
3334
}
3435

@@ -39,12 +40,13 @@ func TestMain(m *testing.M) {
3940
}
4041

4142
const (
42-
authToken = "auth_token"
43-
stdoutContents = "some_output"
44-
imageContents = "image_contents"
45-
buildPath = "/v1/build"
46-
wsPath = "/v1/build-ws/"
47-
imagePath = "/v1/image"
43+
authToken = "auth_token"
44+
stdoutContents = "some_output"
45+
imageContents = "image_contents"
46+
buildPath = "/v1/build"
47+
wsPath = "/v1/build-ws/"
48+
imagePath = "/v1/image"
49+
buildCancelSuffix = "/_cancel"
4850
)
4951

5052
func newResponse(m *mockService, id string, def []byte, libraryRef string) client.BuildInfo {
@@ -110,6 +112,15 @@ func (m *mockService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
110112
m.t.Fatal(err)
111113
}
112114
}
115+
} else if r.Method == http.MethodPut && strings.HasSuffix(r.RequestURI, buildCancelSuffix) {
116+
// Mock build cancellation endpoint
117+
if m.cancelResponseCode == http.StatusNoContent {
118+
w.WriteHeader(http.StatusNoContent)
119+
} else {
120+
if err := jsonresp.WriteError(w, "", m.cancelResponseCode); err != nil {
121+
m.t.Fatal(err)
122+
}
123+
}
113124
} else {
114125
w.WriteHeader(http.StatusNotFound)
115126
}

client/output.go

+21-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
"fmt"
1111
"net/http"
1212
"net/url"
13+
"os"
14+
"os/signal"
15+
"syscall"
1316

1417
"github.com/gorilla/websocket"
1518
)
@@ -37,21 +40,32 @@ func (c *Client) GetOutput(ctx context.Context, buildID string, or OutputReader)
3740
h := http.Header{}
3841
c.setRequestHeaders(h)
3942

40-
ws, resp, err := websocket.DefaultDialer.Dial(u.String(), h)
43+
ctx, cancel := context.WithCancel(ctx)
44+
defer cancel()
45+
46+
ws, resp, err := websocket.DefaultDialer.DialContext(ctx, u.String(), h)
4147
if err != nil {
4248
c.Logger.Logf("websocket dial err - %s, partial response: %+v", err, resp)
4349
return err
4450
}
51+
defer resp.Body.Close()
4552
defer ws.Close()
4653

47-
for {
48-
// Check if context has expired
49-
select {
50-
case <-ctx.Done():
51-
return ctx.Err()
52-
default:
54+
go func() {
55+
sigCh := make(chan os.Signal, 1)
56+
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
57+
58+
fmt.Printf("\rShutting down due to signal: %v\n", <-sigCh)
59+
60+
if err := c.Cancel(ctx, buildID); err != nil {
61+
c.Logger.Logf("build cancellation request failed: %v", err)
5362
}
5463

64+
cancel()
65+
66+
}()
67+
68+
for {
5569
// Read from websocket
5670
mt, msg, err := ws.ReadMessage()
5771
if err != nil {

client/status.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
// GetStatus gets the status of a build from the Build Service by build ID
1616
func (c *Client) GetStatus(ctx context.Context, buildID string) (bi BuildInfo, err error) {
17-
req, err := c.newRequest(http.MethodGet, "/v1/build/"+buildID, "", nil)
17+
req, err := c.newRequest(http.MethodGet, "/v1/build/"+buildID, nil)
1818
if err != nil {
1919
return
2020
}

client/version.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type VersionInfo struct {
2222
// GetVersion gets version information from the build service. The context
2323
// controls the lifetime of the request.
2424
func (c *Client) GetVersion(ctx context.Context) (vi VersionInfo, err error) {
25-
req, err := c.newRequest(http.MethodGet, pathVersion, "", nil)
25+
req, err := c.newRequest(http.MethodGet, pathVersion, nil)
2626
if err != nil {
2727
return VersionInfo{}, err
2828
}

0 commit comments

Comments
 (0)