Use a dependency graph to start services

The algorithm is like so:
* get all the leaves of the graph, these are all the service that don't have any dependency
* once a service is started we take the list of its parents (dependents)
* if all the dependencies of each of those dependents are started then we can start it as well
* if not then we continue to the next dependent

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
This commit is contained in:
Djordje Lukic 2020-11-21 22:30:32 +01:00
parent 162c6036b2
commit 1f43b83409
3 changed files with 149 additions and 73 deletions

View File

@ -174,7 +174,7 @@ func (s *local) Down(ctx context.Context, projectName string) error {
return err return err
} }
eg, ctx := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
for _, c := range list { for _, c := range list {
container := c container := c
@ -625,7 +625,7 @@ func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error
StatusText: "Create", StatusText: "Create",
Done: false, Done: false,
}) })
if _, err := s.containerService.apiClient.NetworkCreate(context.Background(), n.Name, createOpts); err != nil { if _, err := s.containerService.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
return errors.Wrapf(err, "failed to create network %s", n.Name) return errors.Wrapf(err, "failed to create network %s", n.Name)
} }
w.Event(progress.Event{ w.Event(progress.Event{

View File

@ -57,7 +57,7 @@ func (s *local) ensureService(ctx context.Context, project *types.Project, servi
scale := getScale(service) scale := getScale(service)
eg, ctx := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
if len(actual) < scale { if len(actual) < scale {
next, err := nextContainerNumber(actual) next, err := nextContainerNumber(actual)
if err != nil { if err != nil {
@ -115,7 +115,7 @@ func (s *local) ensureService(ctx context.Context, project *types.Project, servi
} }
func (s *local) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error { func (s *local) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
eg, ctx := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
for dep, config := range service.DependsOn { for dep, config := range service.DependsOn {
switch config.Condition { switch config.Condition {
case "service_healthy": case "service_healthy":

View File

@ -20,99 +20,175 @@ package local
import ( import (
"context" "context"
"fmt"
"sync"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func inDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, types.ServiceConfig) error) error { type ServiceStatus int
graph := buildDependencyGraph(project.Services)
const (
ServiceStopped ServiceStatus = iota
ServiceStarted
)
func inDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, types.ServiceConfig) error) error {
g := NewGraph(project.Services)
leaves := g.Leaves()
eg, _ := errgroup.WithContext(ctx)
eg.Go(func() error {
return run(ctx, g, eg, leaves, fn)
})
eg, ctx := errgroup.WithContext(ctx)
results := make(chan string)
errors := make(chan error)
scheduled := map[string]bool{}
for len(graph) > 0 {
for _, n := range graph.independents() {
service := n.service
if scheduled[service.Name] {
continue
}
eg.Go(func() error {
err := fn(ctx, service)
if err != nil {
errors <- err
return err
}
results <- service.Name
return nil
})
scheduled[service.Name] = true
}
select {
case result := <-results:
graph.resolved(result)
case err := <-errors:
return err
}
}
return eg.Wait() return eg.Wait()
} }
type dependencyGraph map[string]node // Note: this could be `graph.walk` or whatever
func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, fn func(context.Context, types.ServiceConfig) error) error {
type node struct { for _, node := range nodes {
service types.ServiceConfig n := node
dependencies []string // Don't start this service yet if all of their children have
dependent []string // not been started yet.
} if len(graph.FilterChildren(n.Service.Name, ServiceStopped)) != 0 {
continue
func (graph dependencyGraph) independents() []node {
var nodes []node
for _, node := range graph {
if len(node.dependencies) == 0 {
nodes = append(nodes, node)
} }
eg.Go(func() error {
err := fn(ctx, n.Service)
if err != nil {
return err
}
graph.UpdateStatus(n.Service.Name, ServiceStarted)
return run(ctx, graph, eg, n.GetParents(), fn)
})
} }
return nodes
return nil
} }
func (graph dependencyGraph) resolved(result string) { type Graph struct {
for _, parent := range graph[result].dependent { Vertices map[string]*Vertex
node := graph[parent] lock sync.RWMutex
node.dependencies = remove(node.dependencies, result)
graph[parent] = node
}
delete(graph, result)
} }
func buildDependencyGraph(services types.Services) dependencyGraph { type Vertex struct {
graph := dependencyGraph{} Key string
for _, s := range services { Service types.ServiceConfig
graph[s.Name] = node{ Status ServiceStatus
service: s, Children map[string]*Vertex
} Parents map[string]*Vertex
}
func (v *Vertex) GetParents() []*Vertex {
var res []*Vertex
for _, p := range v.Parents {
res = append(res, p)
}
return res
}
func NewGraph(services types.Services) *Graph {
graph := &Graph{
lock: sync.RWMutex{},
Vertices: map[string]*Vertex{},
}
for _, s := range services {
graph.AddVertex(s.Name, s)
} }
for _, s := range services { for _, s := range services {
node := graph[s.Name]
for _, name := range s.GetDependencies() { for _, name := range s.GetDependencies() {
dependency := graph[name] graph.AddEdge(s.Name, name)
node.dependencies = append(node.dependencies, name)
dependency.dependent = append(dependency.dependent, s.Name)
graph[name] = dependency
} }
graph[s.Name] = node
} }
return graph return graph
} }
func remove(slice []string, item string) []string { // We then create a constructor function for the Vertex
var s []string func NewVertex(key string, service types.ServiceConfig) *Vertex {
for _, i := range slice { return &Vertex{
if i != item { Key: key,
s = append(s, i) Service: service,
Status: ServiceStopped,
Parents: map[string]*Vertex{},
Children: map[string]*Vertex{},
}
}
func (g *Graph) AddVertex(key string, service types.ServiceConfig) {
g.lock.Lock()
defer g.lock.Unlock()
v := NewVertex(key, service)
g.Vertices[key] = v
}
func (g *Graph) AddEdge(source string, destination string) error {
g.lock.Lock()
defer g.lock.Unlock()
sourceVertex := g.Vertices[source]
destinationVertex := g.Vertices[destination]
if sourceVertex == nil {
return fmt.Errorf("could not find %s", source)
}
if destinationVertex == nil {
return fmt.Errorf("could not find %s", destination)
}
// If they are already connected
if _, ok := sourceVertex.Children[destination]; ok {
return nil
}
sourceVertex.Children[destination] = destinationVertex
destinationVertex.Parents[source] = sourceVertex
g.Vertices[source] = sourceVertex
g.Vertices[destination] = destinationVertex
return nil
}
func (g *Graph) Leaves() []*Vertex {
g.lock.Lock()
defer g.lock.Unlock()
var res []*Vertex
for _, v := range g.Vertices {
if len(v.Children) == 0 {
res = append(res, v)
} }
} }
return s
return res
}
func (g *Graph) UpdateStatus(key string, status ServiceStatus) {
g.lock.Lock()
defer g.lock.Unlock()
g.Vertices[key].Status = status
}
func (g *Graph) FilterChildren(key string, status ServiceStatus) []*Vertex {
g.lock.Lock()
defer g.lock.Unlock()
var res []*Vertex
vertex := g.Vertices[key]
for _, child := range vertex.Children {
if child.Status == status {
res = append(res, child)
}
}
return res
} }