Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 6c2e59b

Browse files
committed
Add new clog.ErrGroup
1 parent 6f8b9b8 commit 6c2e59b

File tree

3 files changed

+72
-28
lines changed

3 files changed

+72
-28
lines changed

internal/clog/doc.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// package clog provides rich error types and logging helpers for coder-cli.
2+
//
3+
// clog encourages returning error types rather than
4+
// logging them and failing with os.Exit as they happen.
5+
// Error, Fatal, and Warn allow downstream functions to return errors with rich formatting information
6+
// while preserving the original, single-line error chain.
7+
package clog

internal/clog/errgroup.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package clog
2+
3+
import (
4+
"fmt"
5+
"sync/atomic"
6+
7+
"golang.org/x/sync/errgroup"
8+
"golang.org/x/xerrors"
9+
)
10+
11+
// ErrGroup wraps the /x/sync/errgroup.(Group) and adds clog logging and rich error propagation.
12+
//
13+
// Take for example, a case in which we are concurrently stopping a slice of environments.
14+
// In this case, we want to log errors as they happen, not pass them through the callstack as errors.
15+
// When the operations complete, we want to log how many, if any, failed. The caller is still expected
16+
// to handle success and info logging.
17+
type ErrGroup interface {
18+
Go(f func() error)
19+
Wait() error
20+
}
21+
22+
type group struct {
23+
egroup errgroup.Group
24+
failures int32
25+
}
26+
27+
// LoggedErrGroup gives an error group with error logging and error propagation handled automatically.
28+
func LoggedErrGroup() ErrGroup {
29+
return &group{
30+
egroup: errgroup.Group{},
31+
failures: 0,
32+
}
33+
}
34+
35+
func (g *group) Go(f func() error) {
36+
g.egroup.Go(func() error {
37+
if err := f(); err != nil {
38+
atomic.AddInt32(&g.failures, 1)
39+
Log(err)
40+
41+
// this error does not matter because we discard it in Wait.
42+
return xerrors.New("")
43+
}
44+
return nil
45+
})
46+
}
47+
48+
func (g *group) Wait() error {
49+
_ = g.egroup.Wait() // ignore this error because we are already tracking failures manually
50+
if g.failures == 0 {
51+
return nil
52+
}
53+
failureWord := "failure"
54+
if g.failures > 1 {
55+
failureWord += "s"
56+
}
57+
return Fatal(fmt.Sprintf("%d %s emitted", g.failures, failureWord))
58+
}

internal/cmd/envs.go

+7-28
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7-
"sync/atomic"
87

98
"cdr.dev/coder-cli/coder-sdk"
109
"cdr.dev/coder-cli/internal/clog"
1110
"cdr.dev/coder-cli/internal/x/xtabwriter"
1211
"github.com/manifoldco/promptui"
1312
"github.com/spf13/cobra"
14-
"golang.org/x/sync/errgroup"
1513
"golang.org/x/xerrors"
1614
)
1715

@@ -113,36 +111,27 @@ coder envs --user charlie@coder.com ls -o json \
113111
return xerrors.Errorf("new client: %w", err)
114112
}
115113

116-
var egroup errgroup.Group
117-
var fails int32
114+
egroup := clog.LoggedErrGroup()
118115
for _, envName := range args {
119116
envName := envName
120117
egroup.Go(func() error {
121118
env, err := findEnv(cmd.Context(), client, envName, *user)
122119
if err != nil {
123-
atomic.AddInt32(&fails, 1)
124-
clog.Log(err)
125-
return xerrors.Errorf("find env by name: %w", err)
120+
return err
126121
}
127122

128123
if err = client.StopEnvironment(cmd.Context(), env.ID); err != nil {
129-
atomic.AddInt32(&fails, 1)
130-
err = clog.Fatal(fmt.Sprintf("stop environment %q", env.Name),
124+
return clog.Error(fmt.Sprintf("stop environment %q", env.Name),
131125
clog.Causef(err.Error()), clog.BlankLine,
132126
clog.Hintf("current environment status is %q", env.LatestStat.ContainerStatus),
133127
)
134-
clog.Log(err)
135-
return err
136128
}
137129
clog.LogSuccess(fmt.Sprintf("successfully stopped environment %q", envName))
138130
return nil
139131
})
140132
}
141133

142-
if err = egroup.Wait(); err != nil {
143-
return clog.Fatal(fmt.Sprintf("%d failure(s) emitted", fails))
144-
}
145-
return nil
134+
return egroup.Wait()
146135
},
147136
}
148137
}
@@ -353,35 +342,25 @@ func rmEnvsCommand(user *string) *cobra.Command {
353342
}
354343
}
355344

356-
var egroup errgroup.Group
357-
var failures int32
345+
egroup := clog.LoggedErrGroup()
358346
for _, envName := range args {
359347
envName := envName
360348
egroup.Go(func() error {
361349
env, err := findEnv(ctx, client, envName, *user)
362350
if err != nil {
363-
atomic.AddInt32(&failures, 1)
364-
clog.Log(err)
365351
return err
366352
}
367353
if err = client.DeleteEnvironment(cmd.Context(), env.ID); err != nil {
368-
atomic.AddInt32(&failures, 1)
369-
err = clog.Error(
354+
return clog.Error(
370355
fmt.Sprintf(`failed to delete environment "%s"`, env.Name),
371356
clog.Causef(err.Error()),
372357
)
373-
clog.Log(err)
374-
return err
375358
}
376359
clog.LogSuccess(fmt.Sprintf("deleted environment %q", env.Name))
377360
return nil
378361
})
379362
}
380-
381-
if err = egroup.Wait(); err != nil {
382-
return xerrors.Errorf("%d failure(s) emitted", failures)
383-
}
384-
return nil
363+
return egroup.Wait()
385364
},
386365
}
387366
cmd.Flags().BoolVarP(&force, "force", "f", false, "force remove the specified environments without prompting first")

0 commit comments

Comments
 (0)