Skip to content

Commit fb145fb

Browse files
committed
Implement Cancel() API for cancelling builds
1 parent 0657e94 commit fb145fb

File tree

4 files changed

+84
-13
lines changed

4 files changed

+84
-13
lines changed

client/build.go

Lines changed: 21 additions & 0 deletions
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"
@@ -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

Lines changed: 26 additions & 0 deletions
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_test.go

Lines changed: 17 additions & 6 deletions
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

Lines changed: 20 additions & 7 deletions
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,31 @@ 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
}
4551
defer ws.Close()
4652

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

63+
cancel()
64+
65+
}()
66+
67+
for {
5568
// Read from websocket
5669
mt, msg, err := ws.ReadMessage()
5770
if err != nil {

0 commit comments

Comments
 (0)