Skip to content

Commit 2b3eaa5

Browse files
committed
refactor SetOnce[struct{}] to ErrorOnce
1 parent 19aa6b8 commit 2b3eaa5

File tree

8 files changed

+139
-33
lines changed

8 files changed

+139
-33
lines changed

packages/orchestrator/internal/sandbox/fc/process.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ type Process struct {
5555
kernelPath string
5656
files *storage.SandboxFiles
5757

58-
Exit *utils.SetOnce[struct{}]
58+
Exit *utils.ErrorOnce
5959

6060
client *apiClient
6161
}
@@ -110,7 +110,7 @@ func NewProcess(
110110

111111
return &Process{
112112
Versions: versions,
113-
Exit: utils.NewSetOnce[struct{}](),
113+
Exit: utils.NewErrorOnce(),
114114
cmd: cmd,
115115
firecrackerSocketPath: files.SandboxFirecrackerSocketPath(),
116116
client: newApiClient(files.SandboxFirecrackerSocketPath()),
@@ -170,7 +170,7 @@ func (p *Process) configure(
170170
if errors.As(waitErr, &exitErr) {
171171
// Check if the process was killed by a signal
172172
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok && status.Signaled() && (status.Signal() == syscall.SIGKILL || status.Signal() == syscall.SIGTERM) {
173-
p.Exit.SetValue(struct{}{})
173+
p.Exit.SetError(nil)
174174

175175
return
176176
}
@@ -186,7 +186,7 @@ func (p *Process) configure(
186186
return
187187
}
188188

189-
p.Exit.SetValue(struct{}{})
189+
p.Exit.SetError(nil)
190190
}()
191191

192192
// Wait for the FC process to start so we can use FC API
@@ -435,7 +435,7 @@ func (p *Process) Stop() error {
435435
}
436436

437437
// If the FC process exited, we can return.
438-
case <-p.Exit.Done:
438+
case <-p.Exit.Done():
439439
return
440440
}
441441
}()

packages/orchestrator/internal/sandbox/sandbox.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ type Sandbox struct {
9999

100100
APIStoredConfig *orchestrator.SandboxConfig
101101

102-
exit *utils.SetOnce[struct{}]
102+
exit *utils.ErrorOnce
103103
}
104104

105105
func (s *Sandbox) LoggerMetadata() sbxlogger.SandboxMetadata {
@@ -132,7 +132,7 @@ func CreateSandbox(
132132
childCtx, childSpan := tracer.Start(ctx, "create-sandbox")
133133
defer childSpan.End()
134134

135-
exit := utils.NewSetOnce[struct{}]()
135+
exit := utils.NewErrorOnce()
136136

137137
cleanup := NewCleanup()
138138
defer func() {
@@ -292,10 +292,10 @@ func CreateSandbox(
292292

293293
go func() {
294294
// If the process exists, stop the sandbox properly
295-
_, fcErr := fcHandle.Exit.Wait()
295+
fcErr := fcHandle.Exit.Wait()
296296
err := sbx.Stop(context.WithoutCancel(ctx), tracer)
297297

298-
exit.SetResult(struct{}{}, errors.Join(err, fcErr))
298+
exit.SetError(errors.Join(err, fcErr))
299299
}()
300300

301301
return sbx, nil
@@ -320,7 +320,7 @@ func ResumeSandbox(
320320
childCtx, childSpan := tracer.Start(ctx, "resume-sandbox")
321321
defer childSpan.End()
322322

323-
exit := utils.NewSetOnce[struct{}]()
323+
exit := utils.NewErrorOnce()
324324

325325
cleanup := NewCleanup()
326326
defer func() {
@@ -399,7 +399,7 @@ func ResumeSandbox(
399399
defer cancelUffdStartCtx(fmt.Errorf("uffd finished starting"))
400400

401401
go func() {
402-
_, uffdWaitErr := fcUffd.Exit().Wait()
402+
uffdWaitErr := fcUffd.Exit().Wait()
403403

404404
cancelUffdStartCtx(fmt.Errorf("uffd process exited: %w", errors.Join(uffdWaitErr, context.Cause(uffdStartCtx))))
405405
}()
@@ -521,23 +521,22 @@ func ResumeSandbox(
521521
go func() {
522522
// Wait for either uffd or fc process to exit
523523
select {
524-
case <-fcUffd.Exit().Done:
525-
case <-fcHandle.Exit.Done:
524+
case <-fcUffd.Exit().Done():
525+
case <-fcHandle.Exit.Done():
526526
}
527527

528528
err := sbx.Stop(context.WithoutCancel(ctx), tracer)
529529

530-
_, uffdWaitErr := fcUffd.Exit().Wait()
531-
_, fcErr := fcHandle.Exit.Wait()
532-
exit.SetResult(struct{}{}, errors.Join(err, fcErr, uffdWaitErr))
530+
uffdWaitErr := fcUffd.Exit().Wait()
531+
fcErr := fcHandle.Exit.Wait()
532+
exit.SetError(errors.Join(err, fcErr, uffdWaitErr))
533533
}()
534534

535535
return sbx, nil
536536
}
537537

538538
func (s *Sandbox) Wait(ctx context.Context) error {
539-
_, err := s.exit.WaitWithContext(ctx)
540-
return err
539+
return s.exit.WaitWithContext(ctx)
541540
}
542541

543542
func (s *Sandbox) Close(ctx context.Context) error {
@@ -565,7 +564,7 @@ func (s *Sandbox) Stop(ctx context.Context, tracer trace.Tracer) error {
565564

566565
// The process exited, we can continue with the rest of the cleanup.
567566
// We could use select with ctx.Done() to wait for cancellation, but if the process is not exited the whole cleanup will be in a bad state and will result in unexpected behavior.
568-
<-s.process.Exit.Done
567+
<-s.process.Exit.Done()
569568

570569
uffdStopErr := s.Resources.memory.Stop()
571570
if uffdStopErr != nil {
@@ -907,8 +906,8 @@ func (s *Sandbox) WaitForExit(
907906
return fmt.Errorf("waiting for exit took too long")
908907
case <-ctx.Done():
909908
return nil
910-
case <-s.exit.Done:
911-
_, err := s.exit.Result()
909+
case <-s.exit.Done():
910+
err := s.exit.Error()
912911
if err == nil {
913912
return nil
914913
}
@@ -942,8 +941,8 @@ func (s *Sandbox) WaitForEnvd(
942941
syncCancel(fmt.Errorf("syncing took too long"))
943942
case <-syncCtx.Done():
944943
return
945-
case <-s.process.Exit.Done:
946-
_, err := s.process.Exit.Result()
944+
case <-s.process.Exit.Done():
945+
err := s.process.Exit.Error()
947946

948947
syncCancel(fmt.Errorf("fc process exited prematurely: %w", err))
949948
}

packages/orchestrator/internal/sandbox/uffd/handler.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
)
2727

2828
type Uffd struct {
29-
exit *utils.SetOnce[struct{}]
29+
exit *utils.ErrorOnce
3030
readyCh chan struct{}
3131

3232
fdExit *fdexit.FdExit
@@ -49,7 +49,7 @@ func New(memfile block.ReadonlyDevice, socketPath string, blockSize int64) (*Uff
4949
}
5050

5151
return &Uffd{
52-
exit: utils.NewSetOnce[struct{}](),
52+
exit: utils.NewErrorOnce(),
5353
readyCh: make(chan struct{}, 1),
5454
fdExit: fdExit,
5555
memfile: trackedMemfile,
@@ -76,7 +76,7 @@ func (u *Uffd) Start(sandboxId string) error {
7676
closeErr := u.lis.Close()
7777
fdExitErr := u.fdExit.Close()
7878

79-
u.exit.SetResult(struct{}{}, errors.Join(handleErr, closeErr, fdExitErr))
79+
u.exit.SetError(errors.Join(handleErr, closeErr, fdExitErr))
8080

8181
close(u.readyCh)
8282
}()
@@ -165,7 +165,7 @@ func (u *Uffd) Ready() chan struct{} {
165165
return u.readyCh
166166
}
167167

168-
func (u *Uffd) Exit() *utils.SetOnce[struct{}] {
168+
func (u *Uffd) Exit() *utils.ErrorOnce {
169169
return u.exit
170170
}
171171

packages/orchestrator/internal/sandbox/uffd/memory_backend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ type MemoryBackend interface {
1313
Start(sandboxId string) error
1414
Stop() error
1515
Ready() chan struct{}
16-
Exit() *utils.SetOnce[struct{}]
16+
Exit() *utils.ErrorOnce
1717
}

packages/orchestrator/internal/sandbox/uffd/noop.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type NoopMemory struct {
1313

1414
dirty *bitset.BitSet
1515

16-
exit *utils.SetOnce[struct{}]
16+
exit *utils.ErrorOnce
1717
}
1818

1919
func NewNoopMemory(size, blockSize int64) *NoopMemory {
@@ -26,7 +26,7 @@ func NewNoopMemory(size, blockSize int64) *NoopMemory {
2626
size: size,
2727
blockSize: blockSize,
2828
dirty: dirty,
29-
exit: utils.NewSetOnce[struct{}](),
29+
exit: utils.NewErrorOnce(),
3030
}
3131
}
3232

@@ -43,7 +43,7 @@ func (m *NoopMemory) Start(sandboxId string) error {
4343
}
4444

4545
func (m *NoopMemory) Stop() error {
46-
return m.exit.SetValue(struct{}{})
46+
return m.exit.SetSuccess()
4747
}
4848

4949
func (m *NoopMemory) Ready() chan struct{} {
@@ -52,6 +52,6 @@ func (m *NoopMemory) Ready() chan struct{} {
5252
return ch
5353
}
5454

55-
func (m *NoopMemory) Exit() *utils.SetOnce[struct{}] {
55+
func (m *NoopMemory) Exit() *utils.ErrorOnce {
5656
return m.exit
5757
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package utils
2+
3+
import "context"
4+
5+
// ErrorOnce is a wrapper around SetOnce that can only be set with an error.
6+
// It's useful for cases where you only need to signal completion with a potential error,
7+
// without carrying any value.
8+
type ErrorOnce struct {
9+
setOnce *SetOnce[struct{}]
10+
}
11+
12+
// NewErrorOnce creates a new ErrorOnce instance.
13+
func NewErrorOnce() *ErrorOnce {
14+
return &ErrorOnce{
15+
setOnce: NewSetOnce[struct{}](),
16+
}
17+
}
18+
19+
// SetError sets the error once. Subsequent calls will return ErrAlreadySet.
20+
func (e *ErrorOnce) SetError(err error) error {
21+
return e.setOnce.SetError(err)
22+
}
23+
24+
// SetSuccess marks the operation as completed successfully (no error).
25+
// This is equivalent to SetError(nil).
26+
func (e *ErrorOnce) SetSuccess() error {
27+
return e.setOnce.SetError(nil)
28+
}
29+
30+
// Wait blocks until an error is set and returns it.
31+
// Returns nil if the operation completed successfully.
32+
func (e *ErrorOnce) Wait() error {
33+
_, err := e.setOnce.Wait()
34+
return err
35+
}
36+
37+
// Error returns the error if one has been set, or ErrNotSet if not set yet.
38+
// Unlike Wait, this doesn't block.
39+
func (e *ErrorOnce) Error() error {
40+
_, err := e.setOnce.Result()
41+
return err
42+
}
43+
44+
// WaitWithContext waits for an error to be set with context cancellation support.
45+
// Returns the set error or ctx.Err() if the context is cancelled first.
46+
func (e *ErrorOnce) WaitWithContext(ctx context.Context) error {
47+
_, err := e.setOnce.WaitWithContext(ctx)
48+
return err
49+
}
50+
51+
// Done returns a channel that's closed when an error is set.
52+
// This allows using ErrorOnce in select statements.
53+
func (e *ErrorOnce) Done() <-chan struct{} {
54+
return e.setOnce.Done
55+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestErrorOnce(t *testing.T) {
12+
errorOnce := NewErrorOnce()
13+
14+
// Test setting error
15+
expectedErr := fmt.Errorf("test error")
16+
err := errorOnce.SetError(expectedErr)
17+
require.NoError(t, err)
18+
19+
// Wait should return the error
20+
err = errorOnce.Wait()
21+
require.Error(t, err)
22+
assert.Equal(t, expectedErr, err)
23+
24+
// Trying to set again should return ErrAlreadySet
25+
err = errorOnce.SetError(fmt.Errorf("another error"))
26+
require.ErrorIs(t, err, ErrAlreadySet)
27+
28+
// Wait should still return the original error
29+
err = errorOnce.Wait()
30+
require.Error(t, err)
31+
assert.Equal(t, expectedErr, err)
32+
}
33+
34+
func TestErrorOnceSetSuccess(t *testing.T) {
35+
errorOnce := NewErrorOnce()
36+
37+
// Test setting success (nil error)
38+
err := errorOnce.SetSuccess()
39+
require.NoError(t, err)
40+
41+
// Wait should return nil
42+
err = errorOnce.Wait()
43+
require.NoError(t, err)
44+
45+
// Trying to set error after success should return ErrAlreadySet
46+
err = errorOnce.SetError(fmt.Errorf("test error"))
47+
require.ErrorIs(t, err, ErrAlreadySet)
48+
49+
// Wait should still return nil
50+
err = errorOnce.Wait()
51+
require.NoError(t, err)
52+
}

packages/shared/pkg/utils/set_once.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (s *SetOnce[T]) setResult(r result[T]) error {
6767
defer s.mu.Unlock()
6868

6969
if s.res != nil {
70-
return fmt.Errorf("value already set")
70+
return ErrAlreadySet
7171
}
7272

7373
s.res = &r

0 commit comments

Comments
 (0)