@@ -18,10 +18,12 @@ package compose
18
18
19
19
import (
20
20
"context"
21
+ "errors"
21
22
"fmt"
22
23
"os"
23
24
"os/signal"
24
25
"slices"
26
+ "sync"
25
27
"sync/atomic"
26
28
"syscall"
27
29
@@ -33,7 +35,6 @@ import (
33
35
"github.com/docker/compose/v2/pkg/api"
34
36
"github.com/docker/compose/v2/pkg/progress"
35
37
"github.com/eiannone/keyboard"
36
- "github.com/hashicorp/go-multierror"
37
38
"github.com/sirupsen/logrus"
38
39
"golang.org/x/sync/errgroup"
39
40
)
@@ -61,14 +62,11 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
61
62
return err
62
63
}
63
64
64
- var eg multierror.Group
65
-
66
65
// if we get a second signal during shutdown, we kill the services
67
66
// immediately, so the channel needs to have sufficient capacity or
68
67
// we might miss a signal while setting up the second channel read
69
68
// (this is also why signal.Notify is used vs signal.NotifyContext)
70
69
signalChan := make (chan os.Signal , 2 )
71
- defer close (signalChan )
72
70
signal .Notify (signalChan , syscall .SIGINT , syscall .SIGTERM )
73
71
defer signal .Stop (signalChan )
74
72
var isTerminated atomic.Bool
@@ -103,26 +101,45 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
103
101
104
102
printer := newLogPrinter (logConsumer )
105
103
106
- doneCh := make (chan bool )
104
+ // global context to handle canceling goroutines
105
+ globalCtx , cancel := context .WithCancel (ctx )
106
+ defer cancel ()
107
+
108
+ var (
109
+ eg errgroup.Group
110
+ mu sync.Mutex
111
+ errs []error
112
+ )
113
+
114
+ appendErr := func (err error ) {
115
+ if err != nil {
116
+ mu .Lock ()
117
+ errs = append (errs , err )
118
+ mu .Unlock ()
119
+ }
120
+ }
121
+
107
122
eg .Go (func () error {
108
123
first := true
109
124
gracefulTeardown := func () {
110
125
first = false
111
126
fmt .Println ("Gracefully Stopping... press Ctrl+C again to force" )
112
127
eg .Go (func () error {
113
- return progress .RunWithLog (context .WithoutCancel (ctx ), func (ctx context.Context ) error {
114
- return s .stop (ctx , project .Name , api.StopOptions {
128
+ err := progress .RunWithLog (context .WithoutCancel (globalCtx ), func (c context.Context ) error {
129
+ return s .stop (c , project .Name , api.StopOptions {
115
130
Services : options .Create .Services ,
116
131
Project : project ,
117
132
}, printer .HandleEvent )
118
133
}, s .stdinfo (), logConsumer )
134
+ appendErr (err )
135
+ return nil
119
136
})
120
137
isTerminated .Store (true )
121
138
}
122
139
123
140
for {
124
141
select {
125
- case <- doneCh :
142
+ case <- globalCtx . Done () :
126
143
if watcher != nil {
127
144
return watcher .Stop ()
128
145
}
@@ -133,12 +150,12 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
133
150
}
134
151
case <- signalChan :
135
152
if first {
136
- keyboard .Close () //nolint:errcheck
153
+ _ = keyboard .Close ()
137
154
gracefulTeardown ()
138
155
break
139
156
}
140
157
eg .Go (func () error {
141
- err := s .kill (context .WithoutCancel (ctx ), project .Name , api.KillOptions {
158
+ err := s .kill (context .WithoutCancel (globalCtx ), project .Name , api.KillOptions {
142
159
Services : options .Create .Services ,
143
160
Project : project ,
144
161
All : true ,
@@ -148,18 +165,21 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
148
165
return nil
149
166
}
150
167
151
- return err
168
+ appendErr (err )
169
+ return nil
152
170
})
153
171
return nil
154
172
case event := <- kEvents :
155
- navigationMenu .HandleKeyEvents (ctx , event , project , options )
173
+ navigationMenu .HandleKeyEvents (globalCtx , event , project , options )
156
174
}
157
175
}
158
176
})
159
177
160
178
if options .Start .Watch && watcher != nil {
161
- err = watcher .Start (ctx )
162
- if err != nil {
179
+ if err := watcher .Start (globalCtx ); err != nil {
180
+ // cancel the global context to terminate background goroutines
181
+ cancel ()
182
+ _ = eg .Wait ()
163
183
return err
164
184
}
165
185
}
@@ -186,12 +206,14 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
186
206
exitCode = event .ExitCode
187
207
_ , _ = fmt .Fprintln (s .stdinfo (), progress .ErrorColor ("Aborting on container exit..." ))
188
208
eg .Go (func () error {
189
- return progress .RunWithLog (context .WithoutCancel (ctx ), func (ctx context.Context ) error {
190
- return s .stop (ctx , project .Name , api.StopOptions {
209
+ err := progress .RunWithLog (context .WithoutCancel (globalCtx ), func (c context.Context ) error {
210
+ return s .stop (c , project .Name , api.StopOptions {
191
211
Services : options .Create .Services ,
192
212
Project : project ,
193
213
}, printer .HandleEvent )
194
214
}, s .stdinfo (), logConsumer )
215
+ appendErr (err )
216
+ return nil
195
217
})
196
218
}
197
219
})
@@ -208,13 +230,10 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
208
230
})
209
231
}
210
232
211
- // use an independent context tied to the errgroup for background attach operations
212
- // the primary context is still used for other operations
213
- // this means that once any attach operation fails, all other attaches are cancelled,
214
- // but an attach failing won't interfere with the rest of the start
215
- _ , attachCtx := errgroup .WithContext (ctx )
216
- containers , err := s .attach (attachCtx , project , printer .HandleEvent , options .Start .AttachTo )
233
+ containers , err := s .attach (globalCtx , project , printer .HandleEvent , options .Start .AttachTo )
217
234
if err != nil {
235
+ cancel ()
236
+ _ = eg .Wait ()
218
237
return err
219
238
}
220
239
attached := make ([]string , len (containers ))
@@ -230,38 +249,46 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
230
249
return
231
250
}
232
251
eg .Go (func () error {
233
- ctr , err := s .apiClient ().ContainerInspect (ctx , event .ID )
252
+ ctr , err := s .apiClient ().ContainerInspect (globalCtx , event .ID )
234
253
if err != nil {
235
- return err
254
+ appendErr (err )
255
+ return nil
236
256
}
237
257
238
- err = s .doLogContainer (ctx , options .Start .Attach , event .Source , ctr , api.LogOptions {
258
+ err = s .doLogContainer (globalCtx , options .Start .Attach , event .Source , ctr , api.LogOptions {
239
259
Follow : true ,
240
260
Since : ctr .State .StartedAt ,
241
261
})
242
262
if errdefs .IsNotImplemented (err ) {
243
263
// container may be configured with logging_driver: none
244
264
// as container already started, we might miss the very first logs. But still better than none
245
- return s .doAttachContainer (ctx , event .Service , event .ID , event .Source , printer .HandleEvent )
265
+ err := s .doAttachContainer (globalCtx , event .Service , event .ID , event .Source , printer .HandleEvent )
266
+ appendErr (err )
267
+ return nil
246
268
}
247
- return err
269
+ appendErr (err )
270
+ return nil
248
271
})
249
272
})
250
273
251
274
eg .Go (func () error {
252
- err := monitor .Start (context .Background ())
253
- // Signal for the signal-handler goroutines to stop
254
- close (doneCh )
255
- return err
275
+ err := monitor .Start (globalCtx )
276
+ // cancel the global context to terminate signal-handler goroutines
277
+ cancel ()
278
+ appendErr (err )
279
+ return nil
256
280
})
257
281
258
282
// We use the parent context without cancellation as we manage sigterm to stop the stack
259
283
err = s .start (context .WithoutCancel (ctx ), project .Name , options .Start , printer .HandleEvent )
260
284
if err != nil && ! isTerminated .Load () { // Ignore error if the process is terminated
285
+ cancel ()
286
+ _ = eg .Wait ()
261
287
return err
262
288
}
263
289
264
- err = eg .Wait ().ErrorOrNil ()
290
+ _ = eg .Wait ()
291
+ err = errors .Join (errs ... )
265
292
if exitCode != 0 {
266
293
errMsg := ""
267
294
if err != nil {
0 commit comments