From 74bbc9dad84cec305db4041063b75de0a4217929 Mon Sep 17 00:00:00 2001 From: "Joel D. Elkins" Date: Fri, 12 Aug 2022 21:16:55 -0500 Subject: [PATCH] Implement rudimentary dependency model with start_order Any container definitions in ccl.toml can be given a `start_order` tag (integer). It is recommended to not put a start_order unless a container depends on another one (e.g. synapse needs postgres), in which case all dependents and dependees should be given a `start_order`, with dependees having a lower number than their dependents. It is guaranteed that the dependees will be started first, although the container startup procedure is outside of our control. Containers without a `start_order` will have their operations applied asynchronously, but those with a start order are started, well, in order from lowest to highest. "Stop" operations are applied in the reverse order. --- cmd/create.go | 1 + cmd/execute.go | 16 +++-- cmd/pull.go | 1 + cmd/recreate.go | 4 ++ cmd/restart.go | 5 +- cmd/rm.go | 2 + cmd/start.go | 1 + cmd/stop.go | 1 + cmd/update.go | 3 + internal/pkg/container/container.go | 99 ++++++++++++----------------- internal/pkg/container/ordering.go | 61 ++++++++++++++++++ 11 files changed, 132 insertions(+), 62 deletions(-) create mode 100644 internal/pkg/container/ordering.go diff --git a/cmd/create.go b/cmd/create.go index f58b690..ed0fa29 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -38,6 +38,7 @@ var createCmd = &cobra.Command{ names or categories. Multiple arguments are supported.`, Run: func(_ *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Start) execForEach(conts, func(c *container.Container) command.CommandSet { return c.CreateCommands() }) }, } diff --git a/cmd/execute.go b/cmd/execute.go index 3ac1f37..70d17af 100644 --- a/cmd/execute.go +++ b/cmd/execute.go @@ -32,9 +32,11 @@ import ( func execForEach(tgts []container.Container, getSet func(*container.Container) command.CommandSet) { var wg sync.WaitGroup for i := range tgts { - wg.Add(1) - go func(cont *container.Container) { - defer wg.Done() + async := !tgts[i].StartOrder.Valid + runSet := func(cont *container.Container) { + if async { + defer wg.Done() + } set := getSet(cont) for _, cmd := range set.Commands { if err := cmd.Execute(output, fake, set.ID); err != nil { @@ -45,7 +47,13 @@ func execForEach(tgts []container.Container, getSet func(*container.Container) c return } } - }(&tgts[i]) + } + if async { + wg.Add(1) + go runSet(&tgts[i]) + } else { + runSet(&tgts[i]) + } } wg.Wait() } diff --git a/cmd/pull.go b/cmd/pull.go index 0e78005..4366a9c 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -39,6 +39,7 @@ affected: the old image will still remain, though untagged, and any defined cont will still use it.`, Run: func(cmd *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Start) execForEach(conts, func(c *container.Container) command.CommandSet { return c.PullCommands() }) }, } diff --git a/cmd/recreate.go b/cmd/recreate.go index d7bad37..a2f9943 100644 --- a/cmd/recreate.go +++ b/cmd/recreate.go @@ -38,7 +38,11 @@ var recreateCmd = &cobra.Command{ one or more container names or categories. If empty, "all" is assumed.`, Run: func(_ *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Stop) + execForEach(conts, func(c *container.Container) command.CommandSet { return c.StopCommands() }) + container.Reorder(conts, container.Start) execForEach(conts, func(c *container.Container) command.CommandSet { return c.RecreateCommands() }) + execForEach(conts, func(c *container.Container) command.CommandSet { return c.ConditionalStartCommands() }) }, } diff --git a/cmd/restart.go b/cmd/restart.go index 9a08e68..24cb0ed 100644 --- a/cmd/restart.go +++ b/cmd/restart.go @@ -38,7 +38,10 @@ var restartCmd = &cobra.Command{ one or more container names or categories. If empty, "all" is assumed.`, Run: func(_ *cobra.Command, args []string) { conts := config.Union(args, contMask) - execForEach(conts, func(c *container.Container) command.CommandSet { return c.RestartCommands() }) + container.Reorder(conts, container.Stop) + execForEach(conts, func(c *container.Container) command.CommandSet { return c.StopCommands() }) + container.Reorder(conts, container.Start) + execForEach(conts, func(c *container.Container) command.CommandSet { return c.StartCommands() }) }, } diff --git a/cmd/rm.go b/cmd/rm.go index 1194bcb..a2ff0f4 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -39,6 +39,8 @@ var rmCmd = &cobra.Command{ If running, they will first be stopped.`, Run: func(cmd *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Stop) + execForEach(conts, func(c *container.Container) command.CommandSet { return c.StopCommands() }) execForEach(conts, func(c *container.Container) command.CommandSet { return c.RemoveCommands() }) }, } diff --git a/cmd/start.go b/cmd/start.go index cba47bb..123c0ef 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -38,6 +38,7 @@ var startCmd = &cobra.Command{ one or more container names or categories. If empty, "all" is assumed.`, Run: func(_ *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Start) execForEach(conts, func(c *container.Container) command.CommandSet { return c.StartCommands() }) }, } diff --git a/cmd/stop.go b/cmd/stop.go index 5794eaa..ddb51c8 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -38,6 +38,7 @@ var stopCmd = &cobra.Command{ one or more container names or categories. If empty, "all" is assumed.`, Run: func(_ *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Stop) execForEach(conts, func(c *container.Container) command.CommandSet { return c.StopCommands() }) }, } diff --git a/cmd/update.go b/cmd/update.go index 53af98e..f6d945a 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -38,7 +38,10 @@ var updateCmd = &cobra.Command{ one or more container names or categories. If empty, "all" is assumed.`, Run: func(_ *cobra.Command, args []string) { conts := config.Union(args, contMask) + container.Reorder(conts, container.Stop) execForEach(conts, func(c *container.Container) command.CommandSet { return c.UpdateCommands() }) + container.Reorder(conts, container.Start) + execForEach(conts, func(c *container.Container) command.CommandSet { return c.ConditionalStartCommands() }) }, } diff --git a/internal/pkg/container/container.go b/internal/pkg/container/container.go index eb81b7a..58f114e 100644 --- a/internal/pkg/container/container.go +++ b/internal/pkg/container/container.go @@ -19,6 +19,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + package container import ( @@ -40,25 +41,27 @@ import ( ) type Container struct { - Category string `toml:"category"` - Name string `toml:"name"` - Image string `toml:"image"` - Hostname string `toml:"hostname,omitempty"` - Command []string `toml:"cmd,omitempty"` - Arguments string `toml:"arguments,omitempty"` - Networks []network.Network `toml:"networks,omitempty"` - Env map[string]string `toml:"env,omitempty"` - Mounts []spec.Mount `toml:"mounts,omitempty"` - Restart string `toml:"restart,omitempty"` - Umask null.Int `toml:"umask,omitempty"` - User string `toml:"user,omitempty"` - ExposeTcp []uint16 `toml:"expose_tcp,omitempty"` - ExposeUdp []uint16 `toml:"expose_udp,omitempty"` - PortsTcp map[uint16]uint16 `toml:"ports,omitempty"` - NetNS string `toml:"netns,omitempty"` + Category string `toml:"category"` + Name string `toml:"name"` + Image string `toml:"image"` + Hostname string `toml:"hostname,omitempty"` + Command []string `toml:"cmd,omitempty"` + Arguments string `toml:"arguments,omitempty"` + Networks []network.Network `toml:"networks,omitempty"` + Env map[string]string `toml:"env,omitempty"` + Mounts []spec.Mount `toml:"mounts,omitempty"` + Restart string `toml:"restart,omitempty"` + Umask null.Int `toml:"umask,omitempty"` + User string `toml:"user,omitempty"` + ExposeTcp []uint16 `toml:"expose_tcp,omitempty"` + ExposeUdp []uint16 `toml:"expose_udp,omitempty"` + PortsTcp map[uint16]uint16 `toml:"ports,omitempty"` + NetNS string `toml:"netns,omitempty"` + StartOrder null.Int `toml:"start_order,omitempty"` conn context.Context cdata *define.InspectContainerData + wasRunning null.Bool } func (c *Container) Init(conn context.Context, nets []network.Network) error { @@ -102,7 +105,9 @@ func (c *Container) Init(conn context.Context, nets []network.Network) error { return fmt.Errorf("conn is nil: %s", c.Name) } c.conn = conn - return c.populateCData() + err := c.populateCData() + c.wasRunning.SetValid(c.IsRunning()) + return err } func (c *Container) LogEntry() *log.Entry { @@ -244,32 +249,14 @@ func (c *Container) CreateCommands() cmd.CommandSet { } func (c *Container) RecreateCommands() cmd.CommandSet { - wasRunning := false return c.newCommandSet("RECREATE", cmd.Commands{ - cmd.NewFunc("stash_run_state", func() error { - wasRunning = c.IsRunning() - return nil - }), - cmd.NewSet(c.StopCommands()), - cmd.NewSet(c.removeCommands()), + cmd.NewSet(c.RemoveCommands()), cmd.NewSet(c.CreateCommands()), - cmd.NewConditional("start_if_was_running", - func() bool { return wasRunning }, - cmd.NewSet(c.StartCommands()), - cmd.NewNop(), - ), - }) -} - -func (c *Container) RemoveCommands() cmd.CommandSet { - return c.newCommandSet("REMOVE", cmd.Commands{ - cmd.NewSet(c.StopCommands()), - cmd.NewSet(c.removeCommands()), }) } // unexported version just removes the container without attempting a stop. -func (c *Container) removeCommands() cmd.CommandSet { +func (c *Container) RemoveCommands() cmd.CommandSet { return c.newCommandSet("remove", cmd.Commands{ cmd.NewFunc("remove_if_exists", func() error { if c.cdata.ID == "" { @@ -293,7 +280,11 @@ func (c *Container) StartCommands() cmd.CommandSet { if err != nil { return err } - _, err = containers.Wait(c.conn, c.cdata.ID, &containers.WaitOptions{Condition: []define.ContainerStatus{define.ContainerStateRunning}}) + _, err = containers.Wait( + c.conn, + c.cdata.ID, + &containers.WaitOptions{Condition: []define.ContainerStatus{define.ContainerStateRunning}}, + ) if err != nil { return err } @@ -312,13 +303,6 @@ func (c *Container) StartCommands() cmd.CommandSet { }) } -func (c *Container) RestartCommands() cmd.CommandSet { - return c.newCommandSet("RESTART", cmd.Commands{ - cmd.NewSet(c.StopCommands()), - cmd.NewSet(c.StartCommands()), - }) -} - func (c *Container) IsRunning() bool { if c.cdata != nil && c.cdata.State != nil { return c.cdata.State.Running @@ -334,21 +318,18 @@ func (c *Container) IsCreated() bool { } func (c *Container) UpdateCommands() cmd.CommandSet { - wasRunning := false return c.newCommandSet("UPDATE", cmd.Commands{ - cmd.NewFunc("pull_image", func() error { - err := c.pull() - if err != nil { - return err - } - wasRunning = c.cdata != nil && c.cdata.State != nil && c.cdata.State.Running - return nil - }), + cmd.NewSet(c.PullCommands()), cmd.NewSet(c.StopCommands()), - cmd.NewSet(c.removeCommands()), + cmd.NewSet(c.RemoveCommands()), cmd.NewSet(c.CreateCommands()), + }) +} + +func (c *Container) ConditionalStartCommands() cmd.CommandSet { + return c.newCommandSet("CONDSTART", cmd.Commands{ cmd.NewConditional("restart_if_was_running", - func() bool { return wasRunning }, + c.wasRunning.ValueOrZero, cmd.NewSet(c.StartCommands()), cmd.NewNop(), ), @@ -367,7 +348,11 @@ func (c *Container) StopCommands() cmd.CommandSet { if err != nil { return err } - _, err = containers.Wait(c.conn, c.cdata.ID, &containers.WaitOptions{Condition: []define.ContainerStatus{define.ContainerStateExited}}) + _, err = containers.Wait( + c.conn, + c.cdata.ID, + &containers.WaitOptions{Condition: []define.ContainerStatus{define.ContainerStateExited}}, + ) if err != nil { return err } diff --git a/internal/pkg/container/ordering.go b/internal/pkg/container/ordering.go new file mode 100644 index 0000000..f5a29ed --- /dev/null +++ b/internal/pkg/container/ordering.go @@ -0,0 +1,61 @@ +/* +Copyright © 2022 Joel D. Elkins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package container + +import "golang.org/x/exp/slices" + +type operation bool + +const ( + Start operation = true + Stop operation = false +) + +func Reorder(conts []Container, op operation) { + // null orderings go first for either start or stop, as they are executed asynchronously + norm := func(a, b Container) bool { + if !a.StartOrder.Valid { + return true + } + if !b.StartOrder.Valid { + return false + } + return a.StartOrder.ValueOrZero() < b.StartOrder.ValueOrZero() + } + rev := func(a, b Container) bool { + if !a.StartOrder.Valid { + return true + } + if !b.StartOrder.Valid { + return false + } + return b.StartOrder.ValueOrZero() > a.StartOrder.ValueOrZero() + } + var sorter func(a, b Container) bool + if op == Start { + sorter = norm + } else { + sorter = rev + } + slices.SortFunc(conts, sorter) +}