Skip to content

Commit deb9a0c

Browse files
authored
[Prism] Fix resource (docker container) leak when shutting down (#35991)
* Fix prism resource leak when shutting down. * Move wg into MultiplexW to avoid changing MakeWorker api * Add timeout while waiting for WaitGroup to finish. * Fix a worker_test. * Make PendingDone a job method and use atomic internally. * Fix comments for exported methods. * Change MakkeWorker to take job id as the first argument. Checking if id is in wg before calling wait. * Fix test
1 parent 04ce0b6 commit deb9a0c

6 files changed

Lines changed: 67 additions & 9 deletions

File tree

sdks/go/pkg/beam/core/runtime/harness/harness.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ func (c *control) handleInstruction(ctx context.Context, req *fnpb.InstructionRe
704704
func parseTimeoutDurationFlag(ctx context.Context, elementProcessingTimeout string) time.Duration {
705705
userSpecifiedTimeout, err := time.ParseDuration(elementProcessingTimeout)
706706
if err != nil {
707-
log.Errorf(ctx, "Failed to parse element_processing_timeout: %v, there will be no timeout for processing an element in a PTransform operation", err)
707+
log.Warnf(ctx, "Failed to parse element_processing_timeout: %v, there will be no timeout for processing an element in a PTransform operation", err)
708708
return 0 * time.Minute
709709
}
710710
return userSpecifiedTimeout

sdks/go/pkg/beam/runners/prism/internal/environments.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock
259259
defer rc.Close()
260260
var buf bytes.Buffer
261261
stdcopy.StdCopy(&buf, &buf, rc)
262-
logger.Info("container being killed", slog.Any("cause", context.Cause(ctx)), slog.Any("containerLog", buf))
262+
logger.Info("container being killed", slog.Any("cause", context.Cause(ctx)), slog.String("containerLog", buf.String()))
263263
}
264264
// Can't use command context, since it's already canceled here.
265265
if err := cli.ContainerKill(bgctx, containerID, ""); err != nil {
@@ -288,6 +288,8 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock
288288
}
289289

290290
func processEnvironment(ctx context.Context, logger *slog.Logger, pp *pipepb.ProcessPayload, wk *worker.W) {
291+
defer wk.Stop()
292+
291293
cmd := exec.CommandContext(ctx, pp.GetCommand(), "--id='"+wk.ID+"'", "--provision_endpoint="+wk.Endpoint())
292294
logger.Debug("starting process", "cmd", cmd.String())
293295

sdks/go/pkg/beam/runners/prism/internal/execute.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func RunPipeline(j *jobservices.Job) {
7676
// any related job resources.
7777
defer func() {
7878
j.CancelFn(fmt.Errorf("runPipeline returned, cleaning up"))
79+
j.WaitForCleanUp()
7980
}()
8081

8182
j.SendMsg("running " + j.String())
@@ -95,7 +96,7 @@ func RunPipeline(j *jobservices.Job) {
9596
j.SendMsg("pipeline completed " + j.String())
9697

9798
j.SendMsg("terminating " + j.String())
98-
j.Done()
99+
j.PendingDone()
99100
}
100101

101102
type transformExecuter interface {

sdks/go/pkg/beam/runners/prism/internal/jobservices/job.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type Job struct {
9494
// Logger for this job.
9595
Logger *slog.Logger
9696

97+
pendingDone atomic.Bool // indicate the job is done but waiting for clean-up
98+
9799
metrics metricsStore
98100
mw *worker.MultiplexW
99101
}
@@ -194,6 +196,20 @@ func (j *Job) Canceled() {
194196
j.sendState(jobpb.JobState_CANCELLED)
195197
}
196198

199+
// PendingDone indicates that the job is completed and is waiting for clean-up.
200+
func (j *Job) PendingDone() {
201+
j.pendingDone.Store(true)
202+
}
203+
204+
// WaitForCleanUp waits until all environments relevant to the job are cleaned up.
205+
func (j *Job) WaitForCleanUp() {
206+
j.mw.WaitForCleanUp(j.String())
207+
if j.pendingDone.Load() {
208+
// If there is a pending done, only mark it as done after clean-up
209+
j.Done()
210+
}
211+
}
212+
197213
// Failed indicates that the job completed unsuccessfully.
198214
func (j *Job) Failed(err error) {
199215
slog.Error("job failed", slog.Any("job", j), slog.Any("error", err))
@@ -204,7 +220,7 @@ func (j *Job) Failed(err error) {
204220

205221
// MakeWorker instantiates a worker.W populating environment and pipeline data from the Job.
206222
func (j *Job) MakeWorker(env string) *worker.W {
207-
wk := j.mw.MakeWorker(j.String()+"_"+env, env)
223+
wk := j.mw.MakeWorker(j.String(), env)
208224
wk.EnvPb = j.Pipeline.GetComponents().GetEnvironments()[env]
209225
wk.PipelineOptions = j.PipelineOptions()
210226
wk.JobKey = j.JobKey()

sdks/go/pkg/beam/runners/prism/internal/worker/worker.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"strings"
3030
"sync"
3131
"sync/atomic"
32+
"time"
3233

3334
"github.com/apache/beam/sdks/v2/go/pkg/beam/core"
3435
"github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/coder"
@@ -76,6 +77,7 @@ type W struct {
7677
mu sync.Mutex
7778
activeInstructions map[string]controlResponder // Active instructions keyed by InstructionID
7879
Descriptors map[string]*fnpb.ProcessBundleDescriptor // Stages keyed by PBDID
80+
wg *sync.WaitGroup
7981
}
8082

8183
type controlResponder interface {
@@ -142,6 +144,7 @@ func (wk *W) shutdown() {
142144
func (wk *W) Stop() {
143145
wk.shutdown()
144146
wk.parentPool.delete(wk)
147+
wk.wg.Done()
145148
slog.Debug("stopped", "worker", wk)
146149
}
147150

@@ -709,6 +712,7 @@ type MultiplexW struct {
709712
endpoint string
710713
logger *slog.Logger
711714
pool map[string]*W
715+
wg map[string]*sync.WaitGroup
712716
}
713717

714718
// NewMultiplexW instantiates a new FnAPI server for multiplexing FnAPI requests to a W.
@@ -718,6 +722,7 @@ func NewMultiplexW(lis net.Listener, g *grpc.Server, logger *slog.Logger) *Multi
718722
endpoint: "localhost:" + p,
719723
logger: logger,
720724
pool: make(map[string]*W),
725+
wg: make(map[string]*sync.WaitGroup),
721726
}
722727

723728
fnpb.RegisterBeamFnControlServer(g, mw)
@@ -735,8 +740,12 @@ func NewMultiplexW(lis net.Listener, g *grpc.Server, logger *slog.Logger) *Multi
735740
func (mw *MultiplexW) MakeWorker(id, env string) *W {
736741
mw.mu.Lock()
737742
defer mw.mu.Unlock()
743+
workerId := id + "_" + env
744+
if _, ok := mw.wg[id]; !ok {
745+
mw.wg[id] = &sync.WaitGroup{}
746+
}
738747
w := &W{
739-
ID: id,
748+
ID: workerId,
740749
Env: env,
741750

742751
InstReqs: make(chan *fnpb.InstructionRequest, 10),
@@ -746,8 +755,11 @@ func (mw *MultiplexW) MakeWorker(id, env string) *W {
746755
activeInstructions: make(map[string]controlResponder),
747756
Descriptors: make(map[string]*fnpb.ProcessBundleDescriptor),
748757
parentPool: mw,
758+
wg: mw.wg[id],
749759
}
750-
mw.pool[id] = w
760+
mw.pool[workerId] = w
761+
762+
mw.wg[id].Add(1)
751763
return w
752764
}
753765

@@ -809,6 +821,32 @@ func (mw *MultiplexW) delete(w *W) {
809821
delete(mw.pool, w.ID)
810822
}
811823

824+
// WaitForCleanUp waits until all resources relevant to the job are cleaned up.
825+
func (mw *MultiplexW) WaitForCleanUp(id string) {
826+
mw.mu.Lock()
827+
wg := mw.wg[id]
828+
mw.mu.Unlock()
829+
if wg == nil {
830+
return
831+
}
832+
833+
const cleanUpTimeout = 60 * time.Second
834+
c := make(chan struct{})
835+
go func() {
836+
defer close(c)
837+
wg.Wait()
838+
}()
839+
840+
select {
841+
case <-c: // Waitgroup finishes successfully
842+
slog.Debug("Finished cleaning up job " + id)
843+
return
844+
case <-time.After(cleanUpTimeout): // Timeout
845+
slog.Warn("Timeout when cleaning up job " + id)
846+
return
847+
}
848+
}
849+
812850
func handleUnary[Request any, Response any, Method func(*W, context.Context, *Request) (*Response, error)](mw *MultiplexW, ctx context.Context, req *Request, m Method) (*Response, error) {
813851
w, err := mw.workerFromMetadataCtx(ctx)
814852
if err != nil {

sdks/go/pkg/beam/runners/prism/internal/worker/worker_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestMultiplexW_MakeWorker(t *testing.T) {
4444
if w.parentPool == nil {
4545
t.Errorf("MakeWorker instantiated W with a nil reference to MultiplexW")
4646
}
47-
if got, want := w.ID, "test"; got != want {
47+
if got, want := w.ID, "test_testEnv"; got != want {
4848
t.Errorf("MakeWorker(%q) = %v, want %v", want, got, want)
4949
}
5050
got, ok := w.parentPool.pool[w.ID]
@@ -77,8 +77,8 @@ func TestMultiplexW_workerFromMetadataCtx(t *testing.T) {
7777
},
7878
{
7979
name: "matched worker_id",
80-
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("worker_id", "test")),
81-
want: &W{ID: "test"},
80+
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("worker_id", "test_testEnv")),
81+
want: &W{ID: "test_testEnv"},
8282
},
8383
} {
8484
t.Run(tt.name, func(t *testing.T) {
@@ -525,6 +525,7 @@ func TestWorker_State_MultimapSideInput(t *testing.T) {
525525
func newWorker() *W {
526526
mw := &MultiplexW{
527527
pool: map[string]*W{},
528+
wg: map[string]*sync.WaitGroup{},
528529
}
529530
return mw.MakeWorker("test", "testEnv")
530531
}

0 commit comments

Comments
 (0)