Skip to content

Commit 6a31a17

Browse files
committed
use errGroup to parallelize List commands
1 parent 747d734 commit 6a31a17

11 files changed

Lines changed: 146 additions & 101 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/docker/go-connections v0.6.0
1919
github.com/moby/docker-image-spec v1.3.1
2020
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
21+
golang.org/x/sync v0.20.0
2122
)
2223

2324
require (
@@ -195,7 +196,6 @@ require (
195196
golang.org/x/crypto v0.47.0 // indirect
196197
golang.org/x/net v0.49.0 // indirect
197198
golang.org/x/oauth2 v0.32.0 // indirect
198-
golang.org/x/sync v0.20.0 // indirect
199199
golang.org/x/sys v0.42.0 // indirect
200200
golang.org/x/term v0.39.0 // indirect
201201
golang.org/x/text v0.34.0 // indirect

internal/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ type RunOptions struct {
6565
type ContainerService interface {
6666
List(ctx context.Context) ([]Container, error)
6767
Run(ctx context.Context, image Image, opts RunOptions) (string, error)
68-
Get(ctx context.Context, id string) (*Container, error)
68+
Get(ctx context.Context, id string) (Container, error)
6969
Start(ctx context.Context, id string) error
7070
Stop(ctx context.Context, id string) error
7171
Restart(ctx context.Context, id string) error

internal/client/docker.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414
"github.com/GustavoCaso/docker-dash/internal/config"
1515
)
1616

17+
// parallelInspectLimit caps concurrent inspect calls in List operations.
18+
// Prevents overwhelming SSH transports (MaxStartups / connection reset).
19+
const parallelInspectLimit = 8
20+
1721
// dockerClient connects to a local or remote Docker daemon.
1822
type dockerClient struct {
1923
cli *client.Client

internal/client/docker_container_service.go

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88
"log"
99
"strconv"
1010
"strings"
11+
"sync"
1112
"time"
1213

14+
dockertypes "github.com/docker/docker/api/types"
1315
"github.com/docker/docker/api/types/container"
1416
"github.com/docker/docker/api/types/filters"
1517
"github.com/docker/docker/client"
1618
"github.com/docker/docker/pkg/stdcopy"
1719
"github.com/docker/go-connections/nat"
20+
"golang.org/x/sync/errgroup"
1821
)
1922

2023
// Local Container Service.
@@ -23,70 +26,51 @@ type containerService struct {
2326
}
2427

2528
func (s *containerService) List(ctx context.Context) ([]Container, error) {
26-
log.Printf("[docker] ContainerList: all=true")
27-
containers, listErr := s.cli.ContainerList(ctx, container.ListOptions{All: true})
28-
if listErr != nil {
29-
return nil, listErr
29+
log.Printf("[docker] ContainerList")
30+
du, err := s.cli.DiskUsage(ctx, dockertypes.DiskUsageOptions{
31+
Types: []dockertypes.DiskUsageObject{dockertypes.ContainerObject},
32+
})
33+
34+
if err != nil {
35+
return nil, err
3036
}
3137

38+
containers := du.Containers
39+
3240
result := make([]Container, len(containers))
41+
resultMap := sync.Map{}
42+
group, groupCtx := errgroup.WithContext(ctx)
43+
group.SetLimit(parallelInspectLimit)
44+
3345
for i, c := range containers {
34-
name := ""
35-
if len(c.Names) > 0 {
36-
name = strings.TrimPrefix(c.Names[0], "/")
37-
}
46+
idx := i
3847

39-
state := StateStopped
40-
switch c.State {
41-
case "running":
42-
state = StateRunning
43-
case "paused":
44-
state = StatePaused
45-
case "restarting":
46-
state = StateRestarting
47-
}
48+
group.Go(func() error {
49+
container, containerErr := s.Get(groupCtx, c.ID)
4850

49-
ports := make([]PortMapping, 0)
50-
for _, p := range c.Ports {
51-
if p.PublicPort > 0 {
52-
ports = append(ports, PortMapping{
53-
HostPort: p.PublicPort,
54-
ContainerPort: p.PrivatePort,
55-
Protocol: p.Type,
56-
})
51+
if containerErr != nil {
52+
return containerErr
5753
}
58-
}
5954

60-
mounts := make([]Mount, len(c.Mounts))
61-
for j, m := range c.Mounts {
62-
mounts[j] = Mount{
63-
Type: string(m.Type),
64-
Source: m.Source,
65-
Destination: m.Destination,
66-
}
67-
}
55+
resultMap.Store(idx, container)
6856

69-
ci, err := s.cli.ContainerInspect(ctx, c.ID)
70-
if err != nil {
71-
return nil, err
72-
}
57+
return nil
58+
})
59+
}
7360

74-
health := buildContainerHealth(ci.State.Health)
75-
76-
result[i] = Container{
77-
ID: c.ID,
78-
Name: name,
79-
Image: c.Image,
80-
Status: c.Status,
81-
State: state,
82-
Health: health,
83-
Created: timeFromUnix(c.Created),
84-
Ports: ports,
85-
Mounts: mounts,
86-
}
61+
groupErr := group.Wait()
62+
if groupErr != nil {
63+
return nil, groupErr
8764
}
8865

89-
log.Printf("[docker] ContainerList: returned count=%d err=%v", len(result), listErr)
66+
resultMap.Range(func(key, value any) bool {
67+
idx, _ := key.(int)
68+
c, _ := value.(Container)
69+
result[idx] = c
70+
return true
71+
})
72+
73+
log.Printf("[docker] ContainerList: returned count=%d err=%v", len(result), err)
9074
return result, nil
9175
}
9276

@@ -172,11 +156,11 @@ func (s *containerService) Run(ctx context.Context, img Image, opts RunOptions)
172156
return containerResponse.ID, nil
173157
}
174158

175-
func (s *containerService) Get(ctx context.Context, id string) (*Container, error) {
159+
func (s *containerService) Get(ctx context.Context, id string) (Container, error) {
176160
log.Printf("[docker] ContainerInspect: id=%q", id)
177161
c, err := s.cli.ContainerInspect(ctx, id)
178162
if err != nil {
179-
return nil, err
163+
return Container{}, err
180164
}
181165

182166
state := StateStopped
@@ -192,7 +176,7 @@ func (s *containerService) Get(ctx context.Context, id string) (*Container, erro
192176
var created time.Time
193177
created, err = time.Parse(time.RFC3339Nano, c.Created)
194178
if err != nil {
195-
return nil, fmt.Errorf("parsing container created time: %w", err)
179+
return Container{}, fmt.Errorf("parsing container created time: %w", err)
196180
}
197181

198182
ports := make([]PortMapping, 0)
@@ -241,7 +225,7 @@ func (s *containerService) Get(ctx context.Context, id string) (*Container, erro
241225
}
242226

243227
log.Printf("[docker] ContainerInspect: done")
244-
return &Container{
228+
return Container{
245229
ID: c.ID,
246230
Name: strings.TrimPrefix(c.Name, "/"),
247231
Image: c.Config.Image,

internal/client/docker_image_service.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import (
66
"log"
77
"slices"
88
"strings"
9+
"sync"
910
"time"
1011

12+
dockertypes "github.com/docker/docker/api/types"
1113
"github.com/docker/docker/api/types/filters"
1214
"github.com/docker/docker/api/types/image"
1315
"github.com/docker/docker/client"
16+
"golang.org/x/sync/errgroup"
1417
)
1518

1619
// Local Image Service.
@@ -19,23 +22,47 @@ type imageService struct {
1922
}
2023

2124
func (s *imageService) List(ctx context.Context) ([]Image, error) {
22-
log.Printf("[docker] ImageList: all=true")
23-
images, err := s.cli.ImageList(ctx, image.ListOptions{All: true})
25+
log.Printf("[docker] ImageList")
26+
du, err := s.cli.DiskUsage(ctx, dockertypes.DiskUsageOptions{
27+
Types: []dockertypes.DiskUsageObject{dockertypes.ImageObject},
28+
})
2429
if err != nil {
2530
return nil, err
2631
}
2732

33+
images := du.Images
34+
2835
result := make([]Image, len(images))
36+
resultMap := sync.Map{}
37+
group, groupCtx := errgroup.WithContext(ctx)
38+
group.SetLimit(parallelInspectLimit)
39+
2940
for i, img := range images {
30-
imageData, imageErr := s.get(ctx, img.ID)
31-
if imageErr != nil {
32-
return []Image{}, imageErr
33-
}
41+
idx := i
42+
group.Go(func() error {
43+
imageData, imageErr := s.get(groupCtx, img.ID)
44+
if imageErr != nil {
45+
return imageErr
46+
}
47+
48+
imageData.Containers = img.Containers
49+
resultMap.Store(idx, imageData)
50+
return nil
51+
})
52+
}
3453

35-
imageData.Containers = img.Containers
36-
result[i] = imageData
54+
groupErr := group.Wait()
55+
if groupErr != nil {
56+
return []Image{}, groupErr
3757
}
3858

59+
resultMap.Range(func(key, value any) bool {
60+
idx, _ := key.(int)
61+
img, _ := value.(Image)
62+
result[idx] = img
63+
return true
64+
})
65+
3966
log.Printf("[docker] ImageList: returned count=%d", len(result))
4067
return result, nil
4168
}

internal/client/docker_network_service.go

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package client
33
import (
44
"context"
55
"log"
6+
"sync"
67

78
"github.com/docker/docker/api/types/filters"
89
"github.com/docker/docker/api/types/network"
910
"github.com/docker/docker/client"
11+
"golang.org/x/sync/errgroup"
1012
)
1113

1214
// Local Network Service.
@@ -22,39 +24,67 @@ func (s *networkService) List(ctx context.Context) ([]Network, error) {
2224
}
2325

2426
result := make([]Network, len(networks))
27+
resultMap := sync.Map{}
28+
group, groupCtx := errgroup.WithContext(ctx)
29+
group.SetLimit(parallelInspectLimit)
30+
2531
for i, n := range networks {
26-
inspectResponse, inspectErr := s.cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
32+
idx := i
33+
networkID := n.ID
34+
networkName := n.Name
35+
networkDriver := n.Driver
36+
networkScope := n.Scope
37+
networkInternal := n.Internal
38+
networkCreated := n.Created
39+
ipamConfig := n.IPAM
2740

28-
if inspectErr != nil {
29-
return nil, inspectErr
30-
}
41+
group.Go(func() error {
42+
inspectResponse, inspectErr := s.cli.NetworkInspect(groupCtx, networkID, network.InspectOptions{})
43+
if inspectErr != nil {
44+
return inspectErr
45+
}
3146

32-
subnet := ""
33-
gateway := ""
34-
if len(n.IPAM.Config) > 0 {
35-
subnet = inspectResponse.IPAM.Config[0].Subnet
36-
gateway = inspectResponse.IPAM.Config[0].Gateway
37-
}
38-
connected := make([]NetworkContainer, 0, len(inspectResponse.Containers))
39-
for _, c := range inspectResponse.Containers {
40-
connected = append(connected, NetworkContainer{
41-
Name: c.Name,
42-
IPv4Address: c.IPv4Address,
43-
IPv6Address: c.IPv6Address,
44-
MacAddress: c.MacAddress,
47+
subnet := ""
48+
gateway := ""
49+
if len(ipamConfig.Config) > 0 {
50+
subnet = inspectResponse.IPAM.Config[0].Subnet
51+
gateway = inspectResponse.IPAM.Config[0].Gateway
52+
}
53+
connected := make([]NetworkContainer, 0, len(inspectResponse.Containers))
54+
for _, c := range inspectResponse.Containers {
55+
connected = append(connected, NetworkContainer{
56+
Name: c.Name,
57+
IPv4Address: c.IPv4Address,
58+
IPv6Address: c.IPv6Address,
59+
MacAddress: c.MacAddress,
60+
})
61+
}
62+
resultMap.Store(idx, Network{
63+
ID: networkID,
64+
Name: networkName,
65+
Driver: networkDriver,
66+
Scope: networkScope,
67+
Internal: networkInternal,
68+
Created: networkCreated,
69+
ConnectedContainers: connected,
70+
IPAM: NetworkIPAM{Subnet: subnet, Gateway: gateway},
4571
})
46-
}
47-
result[i] = Network{
48-
ID: n.ID,
49-
Name: n.Name,
50-
Driver: n.Driver,
51-
Scope: n.Scope,
52-
Internal: n.Internal,
53-
Created: n.Created,
54-
ConnectedContainers: connected,
55-
IPAM: NetworkIPAM{Subnet: subnet, Gateway: gateway},
56-
}
72+
return nil
73+
})
74+
}
75+
76+
groupErr := group.Wait()
77+
if groupErr != nil {
78+
return nil, groupErr
5779
}
80+
81+
resultMap.Range(func(key, value any) bool {
82+
idx, _ := key.(int)
83+
net, _ := value.(Network)
84+
result[idx] = net
85+
return true
86+
})
87+
5888
log.Printf("[docker] NetworkList: returned count=%d", len(result))
5989
return result, nil
6090
}

internal/client/docker_volume_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type volumeService struct {
1515
}
1616

1717
func (s *volumeService) List(ctx context.Context) ([]Volume, error) {
18-
log.Printf("[docker] DiskUsage (volumes)")
18+
log.Printf("[docker] VolumeList")
1919
du, err := s.cli.DiskUsage(ctx, dockertypes.DiskUsageOptions{
2020
Types: []dockertypes.DiskUsageObject{dockertypes.VolumeObject},
2121
})

internal/client/mock.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,13 @@ func (s *mockContainerService) Run(_ context.Context, _ Image, _ RunOptions) (st
250250
return "", nil
251251
}
252252

253-
func (s *mockContainerService) Get(ctx context.Context, id string) (*Container, error) {
253+
func (s *mockContainerService) Get(ctx context.Context, id string) (Container, error) {
254254
for _, c := range s.containers {
255255
if c.ID == id || c.Name == id {
256-
return &c, nil
256+
return c, nil
257257
}
258258
}
259-
return nil, fmt.Errorf("container not found: %s", id)
259+
return Container{}, fmt.Errorf("container not found: %s", id)
260260
}
261261

262262
func (s *mockContainerService) Start(ctx context.Context, id string) error {

0 commit comments

Comments
 (0)