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

Commit e9b4f97

Browse files
authored
Add new clog.ErrGroup (#157)
1 parent 6f8b9b8 commit e9b4f97

File tree

3 files changed

+72
-28
lines changed

3 files changed

+72
-28
lines changed

internal/clog/doc.go

Lines changed: 7 additions & 0 deletions
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

Lines changed: 58 additions & 0 deletions
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

Lines changed: 7 additions & 28 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy