Skip to content

Commit c7b2543

Browse files
committed
fix: multiple small fixes for service runners
It all started debugging the issue on Talos being stuck on reboot when `talosctl logs -f kubelet` is being used. Fixes: * abort goroutine runner even if the goroutine doesn't terminate - we have no way to force termination, so at least don't hang forever * align timeouts for apid/trustd for graceful termination - so that at least the service is not SIGKILLed while it does its own graceful shutdown * in stream chunker, act on canceled context immediately instead of relying on `Read` to return: with `logs -f` the reader will block forever waiting for new logs Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
1 parent e33d2f5 commit c7b2543

7 files changed

Lines changed: 81 additions & 13 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ require (
187187
golang.org/x/text v0.20.0
188188
golang.org/x/time v0.8.0
189189
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
190-
google.golang.org/grpc v1.68.0 // do not update to 1.68.0 until we find a way around https://github.com/grpc/grpc-go/pull/7535
190+
google.golang.org/grpc v1.68.0
191191
google.golang.org/protobuf v1.35.2
192192
gopkg.in/yaml.v3 v3.0.1
193193
k8s.io/klog/v2 v2.130.1

internal/app/machined/pkg/system/runner/goroutine/goroutine.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import (
1111
"io"
1212
stdlibruntime "runtime"
1313
"sync"
14+
"time"
1415

1516
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
1617
"github.com/siderolabs/talos/internal/app/machined/pkg/system/events"
1718
"github.com/siderolabs/talos/internal/app/machined/pkg/system/runner"
1819
)
1920

21+
// ErrAborted is returned by the service when it's aborted (doesn't stop on timeout).
22+
var ErrAborted = errors.New("service aborted")
23+
2024
// goroutineRunner is a runner.Runner that runs a service in a goroutine.
2125
type goroutineRunner struct {
2226
main FuncMain
@@ -66,10 +70,32 @@ func (r *goroutineRunner) Run(eventSink events.Recorder) error {
6670

6771
eventSink(events.StateRunning, "Service started as goroutine")
6872

69-
return r.wrappedMain()
73+
errCh := make(chan error)
74+
ctx := r.ctx
75+
76+
go func() {
77+
errCh <- r.wrappedMain(ctx)
78+
}()
79+
80+
select {
81+
case <-r.ctx.Done():
82+
eventSink(events.StateStopping, "Service stopping")
83+
case err := <-errCh:
84+
// service finished on its own
85+
return err
86+
}
87+
88+
select {
89+
case <-time.After(r.opts.GracefulShutdownTimeout * 2):
90+
eventSink(events.StateStopping, "Service hasn't stopped gracefully on timeout, aborting")
91+
92+
return ErrAborted
93+
case err := <-errCh:
94+
return err
95+
}
7096
}
7197

72-
func (r *goroutineRunner) wrappedMain() (err error) {
98+
func (r *goroutineRunner) wrappedMain(ctx context.Context) (err error) {
7399
defer func() {
74100
if r := recover(); r != nil {
75101
buf := make([]byte, 8192)
@@ -87,7 +113,7 @@ func (r *goroutineRunner) wrappedMain() (err error) {
87113

88114
defer writerCloser() //nolint:errcheck
89115

90-
if err = r.main(r.ctx, r.runtime, w); !errors.Is(err, context.Canceled) {
116+
if err = r.main(ctx, r.runtime, w); !errors.Is(err, context.Canceled) {
91117
return err // return error if it's not context.Canceled (service was not aborted)
92118
}
93119

internal/app/machined/pkg/system/runner/goroutine/goroutine_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,38 @@ func (suite *GoroutineSuite) TestStop() {
139139
suite.Assert().NoError(<-errCh)
140140
}
141141

142+
func (suite *GoroutineSuite) TestStuckOnStop() {
143+
r := goroutine.NewRunner(suite.r, "teststop",
144+
func(ctx context.Context, data runtime.Runtime, logger io.Writer) error {
145+
// hanging forever
146+
select {}
147+
},
148+
runner.WithLoggingManager(suite.loggingManager),
149+
runner.WithGracefulShutdownTimeout(10*time.Millisecond),
150+
)
151+
152+
suite.Assert().NoError(r.Open())
153+
154+
defer func() { suite.Assert().NoError(r.Close()) }()
155+
156+
errCh := make(chan error)
157+
158+
go func() {
159+
errCh <- r.Run(MockEventSink)
160+
}()
161+
162+
time.Sleep(20 * time.Millisecond)
163+
164+
select {
165+
case <-errCh:
166+
suite.Require().Fail("should not return yet")
167+
default:
168+
}
169+
170+
suite.Assert().NoError(r.Stop())
171+
suite.Assert().ErrorIs(<-errCh, goroutine.ErrAborted)
172+
}
173+
142174
func (suite *GoroutineSuite) TestRunLogs() {
143175
r := goroutine.NewRunner(suite.r, "logtest",
144176
func(ctx context.Context, data runtime.Runtime, logger io.Writer) error {

internal/app/machined/pkg/system/services/apid.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path/filepath"
1515
"strconv"
1616
"strings"
17+
"time"
1718

1819
"github.com/containerd/containerd/v2/pkg/cap"
1920
"github.com/containerd/containerd/v2/pkg/oci"
@@ -200,6 +201,7 @@ func (o *APID) Runner(r runtime.Runtime) (runner.Runner, error) {
200201
runner.WithLoggingManager(r.Logging()),
201202
runner.WithContainerdAddress(constants.SystemContainerdAddress),
202203
runner.WithEnv(env),
204+
runner.WithGracefulShutdownTimeout(15*time.Second),
203205
runner.WithCgroupPath(constants.CgroupApid),
204206
runner.WithSelinuxLabel(constants.SelinuxLabelApid),
205207
runner.WithOCISpecOpts(

internal/app/machined/pkg/system/services/trustd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"strconv"
16+
"time"
1617

1718
"github.com/containerd/containerd/v2/pkg/cap"
1819
"github.com/containerd/containerd/v2/pkg/oci"
@@ -164,6 +165,7 @@ func (t *Trustd) Runner(r runtime.Runtime) (runner.Runner, error) {
164165
runner.WithContainerdAddress(constants.SystemContainerdAddress),
165166
runner.WithEnv(env),
166167
runner.WithCgroupPath(constants.CgroupTrustd),
168+
runner.WithGracefulShutdownTimeout(15*time.Second),
167169
runner.WithSelinuxLabel(constants.SelinuxLabelTrustd),
168170
runner.WithOCISpecOpts(
169171
oci.WithDroppedCapabilities(cap.Known()),

pkg/chunker/stream/stream.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"os"
1213

1314
"github.com/siderolabs/gen/xslices"
15+
"github.com/siderolabs/go-circular"
1416

1517
"github.com/siderolabs/talos/pkg/chunker"
1618
)
@@ -61,27 +63,35 @@ func NewChunker(ctx context.Context, source Source, setters ...Option) chunker.C
6163
}
6264

6365
// Read implements ChunkReader.
66+
//
67+
//nolint:gocyclo
6468
func (c *Stream) Read() <-chan []byte {
6569
// Create a buffered channel of length 1.
6670
ch := make(chan []byte, 1)
6771

6872
go func(ch chan []byte) {
6973
defer close(ch)
70-
//nolint:errcheck
71-
defer c.source.Close()
74+
75+
ctx, cancel := context.WithCancel(c.ctx)
76+
defer cancel()
77+
78+
go func() {
79+
<-ctx.Done()
80+
c.source.Close() //nolint:errcheck
81+
}()
7282

7383
buf := make([]byte, c.options.Size)
7484

7585
for {
7686
select {
77-
case <-c.ctx.Done():
87+
case <-ctx.Done():
7888
return
7989
default:
8090
}
8191

8292
n, err := c.source.Read(buf)
8393
if err != nil {
84-
if !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe)) {
94+
if !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, os.ErrClosed) || errors.Is(err, circular.ErrClosed)) {
8595
fmt.Printf("read error: %s\n", err.Error())
8696
}
8797

@@ -93,7 +103,7 @@ func (c *Stream) Read() <-chan []byte {
93103
b := xslices.CopyN(buf, n)
94104

95105
select {
96-
case <-c.ctx.Done():
106+
case <-ctx.Done():
97107
return
98108
case ch <- b:
99109
}

pkg/chunker/stream/stream_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,6 @@ func (suite *StreamChunkerSuite) TestStreamingCancel() {
123123

124124
ctxCancel()
125125

126-
// need any I/O for chunker to notice that context got canceled
127-
//nolint:errcheck
128-
suite.writer.Write([]byte(""))
129-
130126
suite.Require().Equal([]byte("abcdefghijklmno"), <-combinedCh)
131127
}
132128

0 commit comments

Comments
 (0)