Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lifecycle/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ func NewFuncActor(start, stop func(ctx context.Context) error) *FuncActor {
}

// WithName sets the name of the actor.
// If a readiness probe is enabled, its name is also updated.
func (f *FuncActor) WithName(name string) *FuncActor {
f.name = name
if f.readinessProbe != nil {
f.readinessProbe.WithName(name)
}
return f
}

Expand Down Expand Up @@ -73,7 +77,7 @@ func (f *FuncActor) RequiresStop() bool {
// Returns the actor for method chaining.
func (f *FuncActor) WithReadiness() *FuncActor {
if f.readinessProbe == nil {
f.readinessProbe = NewReadinessProbe()
f.readinessProbe = NewReadinessProbe().WithName(f.name)
}
return f
}
Expand Down
14 changes: 13 additions & 1 deletion lifecycle/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func (lc *Lifecycle) Start(ctx context.Context) (xerr error) {

lc.mu.RLock()
actors := slices.Clone(lc.actors)
logger := lc.logger
lc.mu.RUnlock()

// Check for long-running services before starting actors
Expand Down Expand Up @@ -177,25 +178,34 @@ func (lc *Lifecycle) Start(ctx context.Context) (xerr error) {
}

// Wait for all probes to signal ready
for _, probe := range allProbes {
for i, probe := range allProbes {
probeName := probe.GetName()
if probeName == "" {
probeName = fmt.Sprintf("probe[%d]", i)
}
logger.DebugContext(ctx, "Waiting for readiness probe", "probe", probeName)
select {
case <-lc.Done():
return lc.Err()
case <-ctx.Done():
return errors.WithStack(ctx.Err())
case <-probe.Done():
if err := probe.Error(); err != nil {
logger.ErrorContext(ctx, "Readiness probe failed", "probe", probeName, "error", err)
return err
}
logger.DebugContext(ctx, "Readiness probe signaled ready", "probe", probeName)
}
}

return nil
}

// WithName sets the name for the lifecycle.
// Also updates the readiness probe name.
func (lc *Lifecycle) WithName(name string) *Lifecycle {
lc.FuncService.WithName(name)
lc.readinessProbe.WithName(name)
return lc
}

Expand Down Expand Up @@ -333,6 +343,8 @@ func (lc *Lifecycle) Serve(ctx context.Context, ctors ...any) (xerr error) {
actorType = "Service"
}

logger.DebugContext(ctx, fmt.Sprintf("%s starting", actorType), "actor", actorName)

if err := actor.Start(ctx); err != nil {
logger.ErrorContext(ctx, fmt.Sprintf("Failed to start %s", strings.ToLower(actorType)), "actor", actorName, "error", err)
return err
Expand Down
12 changes: 12 additions & 0 deletions lifecycle/readiness.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ReadinessProbe struct {
doneC chan struct{} // Completion signal channel
mu sync.RWMutex // Protects err field
err error // Error if failed, nil if successful
name string // Name for logging and debugging
}

// NewReadinessProbe creates a new readiness probe.
Expand Down Expand Up @@ -46,3 +47,14 @@ func (rp *ReadinessProbe) Error() error {
defer rp.mu.RUnlock()
return rp.err
}

// WithName sets the name of the readiness probe for logging and debugging.
func (rp *ReadinessProbe) WithName(name string) *ReadinessProbe {
rp.name = name
return rp
}

// GetName returns the name of the readiness probe.
func (rp *ReadinessProbe) GetName() string {
Comment on lines +52 to +58

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name field is accessed without mutex protection, which could lead to race conditions. The ReadinessProbe struct already has a sync.RWMutex to protect the err field. The name field should be similarly protected since WithName could be called concurrently with GetName. Consider acquiring the appropriate lock (write lock for WithName, read lock for GetName) to ensure thread-safe access.

Suggested change
func (rp *ReadinessProbe) WithName(name string) *ReadinessProbe {
rp.name = name
return rp
}
// GetName returns the name of the readiness probe.
func (rp *ReadinessProbe) GetName() string {
func (rp *ReadinessProbe) WithName(name string) *ReadinessProbe {
rp.mu.Lock()
rp.name = name
rp.mu.Unlock()
return rp
}
// GetName returns the name of the readiness probe.
func (rp *ReadinessProbe) GetName() string {
rp.mu.RLock()
defer rp.mu.RUnlock()

Copilot uses AI. Check for mistakes.
return rp.name
}
Comment on lines +58 to +60

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name field access in GetName is not protected by a mutex, which could lead to race conditions when called concurrently with WithName. Consider acquiring a read lock before accessing the name field to ensure thread-safe access.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +60

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new WithName and GetName methods lack test coverage. Since the readiness_test.go file has comprehensive test coverage for other ReadinessProbe methods, consider adding tests to verify that WithName correctly sets the name and GetName retrieves it, including edge cases like empty names and concurrent access.

Copilot uses AI. Check for mistakes.
Loading