Browse code

client: Client.TaskLogs: close reader on context cancellation

Use a cancelReadCloser to automatically close the reader when the context
is cancelled. Consumers are still recommended to manually close the reader,
but the cancelReadCloser makes the Close idempotent.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/10/30 19:36:29
Showing 2 changed files
... ...
@@ -27,9 +27,14 @@ type TaskLogsResult interface {
27 27
 	io.ReadCloser
28 28
 }
29 29
 
30
-// TaskLogs returns the logs generated by a task.
31
-// It's up to the caller to close the stream.
30
+// TaskLogs returns the logs generated by a service in a [TaskLogsResult].
31
+// as an [io.ReadCloser]. Callers should close the stream.
32
+//
33
+// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
32 34
 func (cli *Client) TaskLogs(ctx context.Context, taskID string, options TaskLogsOptions) (TaskLogsResult, error) {
35
+	// TODO(thaJeztah): this function needs documentation about the format of ths stream (similar to for container logs)
36
+	// TODO(thaJeztah): migrate CLI utilities to the client where suitable; https://github.com/docker/cli/blob/v29.0.0-rc.1/cli/command/service/logs.go#L73-L348
37
+
33 38
 	query := url.Values{}
34 39
 	if options.ShowStdout {
35 40
 		query.Set("stdout", "1")
... ...
@@ -65,30 +70,15 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options TaskLogs
65 65
 		return nil, err
66 66
 	}
67 67
 	return &taskLogsResult{
68
-		body: resp.Body,
68
+		ReadCloser: newCancelReadCloser(ctx, resp.Body),
69 69
 	}, nil
70 70
 }
71 71
 
72 72
 type taskLogsResult struct {
73
-	// body must be closed to avoid a resource leak
74
-	body io.ReadCloser
73
+	io.ReadCloser
75 74
 }
76 75
 
77 76
 var (
78 77
 	_ io.ReadCloser       = (*taskLogsResult)(nil)
79 78
 	_ ContainerLogsResult = (*taskLogsResult)(nil)
80 79
 )
81
-
82
-func (r *taskLogsResult) Read(p []byte) (int, error) {
83
-	if r == nil || r.body == nil {
84
-		return 0, io.EOF
85
-	}
86
-	return r.body.Read(p)
87
-}
88
-
89
-func (r *taskLogsResult) Close() error {
90
-	if r == nil || r.body == nil {
91
-		return nil
92
-	}
93
-	return r.body.Close()
94
-}
... ...
@@ -27,9 +27,14 @@ type TaskLogsResult interface {
27 27
 	io.ReadCloser
28 28
 }
29 29
 
30
-// TaskLogs returns the logs generated by a task.
31
-// It's up to the caller to close the stream.
30
+// TaskLogs returns the logs generated by a service in a [TaskLogsResult].
31
+// as an [io.ReadCloser]. Callers should close the stream.
32
+//
33
+// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
32 34
 func (cli *Client) TaskLogs(ctx context.Context, taskID string, options TaskLogsOptions) (TaskLogsResult, error) {
35
+	// TODO(thaJeztah): this function needs documentation about the format of ths stream (similar to for container logs)
36
+	// TODO(thaJeztah): migrate CLI utilities to the client where suitable; https://github.com/docker/cli/blob/v29.0.0-rc.1/cli/command/service/logs.go#L73-L348
37
+
33 38
 	query := url.Values{}
34 39
 	if options.ShowStdout {
35 40
 		query.Set("stdout", "1")
... ...
@@ -65,30 +70,15 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options TaskLogs
65 65
 		return nil, err
66 66
 	}
67 67
 	return &taskLogsResult{
68
-		body: resp.Body,
68
+		ReadCloser: newCancelReadCloser(ctx, resp.Body),
69 69
 	}, nil
70 70
 }
71 71
 
72 72
 type taskLogsResult struct {
73
-	// body must be closed to avoid a resource leak
74
-	body io.ReadCloser
73
+	io.ReadCloser
75 74
 }
76 75
 
77 76
 var (
78 77
 	_ io.ReadCloser       = (*taskLogsResult)(nil)
79 78
 	_ ContainerLogsResult = (*taskLogsResult)(nil)
80 79
 )
81
-
82
-func (r *taskLogsResult) Read(p []byte) (int, error) {
83
-	if r == nil || r.body == nil {
84
-		return 0, io.EOF
85
-	}
86
-	return r.body.Read(p)
87
-}
88
-
89
-func (r *taskLogsResult) Close() error {
90
-	if r == nil || r.body == nil {
91
-		return nil
92
-	}
93
-	return r.body.Close()
94
-}