From 87b4cc3843e40a10654ffb5c9b95ecca1cfe0d70 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 25 Jan 2018 10:48:36 -0800 Subject: [PATCH] libcontainer: Add support for the "pausing" status The freezer controller distinguishes between freezing and frozen [1]. We've had a "Pausing" since dbb515f0 (Update api proposal, 2014-07-08), but had never implemented it. This commit adds the missing implementation. I haven't updated libcontainer/cgroups, which has used a blocking freeze change since 3e8849fa (implement wait on freeze, 2014-06-04). The cgroups interface doesn't distinguish between blocking and non-blocking sets, so there's no way to say "I want to start freezing, but don't need to block until completion". I haven't added integration tests either. All of our current tests (state.bats and pause.bats) assume a complete transition to 'paused'. That makes sense because 'runc pause ...' is based on the blocking libcontainer/cgroups implementation. The only way to see 'pausing' would be to have a parallel state call during the pause call. Here's how the timing could work out: 1. Call 'pause'. 2. Call 'state', which reads THAWED and outputs 'running' (or 'created', or whatever). 3. 'pause' starts freezing the controller. 4. Call 'state', which reads FREEZING and outputs 'pausing'. 5. 'pause' finishes freezing the controller. 6. Call 'state', which reads FROZEN and outputs 'paused'. 7. 'pause' exits. I'm not sure how to get a test where the 'state' call consistently fires at 4 and not at 2 or 6. [1]: https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt Signed-off-by: W. Trevor King --- libcontainer/container_linux.go | 26 ++++++++++++++------ libcontainer/error.go | 3 +++ libcontainer/state_linux.go | 41 +++++++++++++++++++++++++++++--- libcontainer/state_linux_test.go | 16 +++++++++++++ 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index cfb05b43a80..da64a4b01db 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -1588,11 +1588,13 @@ func (c *linuxContainer) currentStatus() (Status, error) { // out of process we need to verify the container's status based on runtime // information and not rely on our in process info. func (c *linuxContainer) refreshState() error { - paused, err := c.isPaused() + paused, err := c.pausedState() if err != nil { return err } - if paused { + if paused == Pausing { + return c.state.transition(&pausingState{c: c}) + } else if paused == Paused { return c.state.transition(&pausedState{c: c}) } t, err := c.runType() @@ -1628,21 +1630,31 @@ func (c *linuxContainer) runType() (Status, error) { return Running, nil } -func (c *linuxContainer) isPaused() (bool, error) { +// pausedState returns -1 (for containers that are neither Pausing or +// Paused), Pausing, or Paused. +func (c *linuxContainer) pausedState() (Status, error) { fcg := c.cgroupManager.GetPaths()["freezer"] if fcg == "" { // A container doesn't have a freezer cgroup - return false, nil + return -1, nil } data, err := ioutil.ReadFile(filepath.Join(fcg, "freezer.state")) if err != nil { // If freezer cgroup is not mounted, the container would just be not paused. if os.IsNotExist(err) { - return false, nil + return -1, nil } - return false, newSystemErrorWithCause(err, "checking if container is paused") + return -1, newSystemErrorWithCause(err, "checking if container is paused") } - return bytes.Equal(bytes.TrimSpace(data), []byte("FROZEN")), nil + state := string(bytes.TrimSpace(data)) + if state == "FREEZING" { + return Pausing, nil + } else if state == "FROZEN" { + return Paused, nil + } else if state == "THAWED" { + return -1, nil + } + return -1, fmt.Errorf("unrecognized freezer.state: %s", state) } func (c *linuxContainer) currentState() (*State, error) { diff --git a/libcontainer/error.go b/libcontainer/error.go index 21a3789ba18..95976f58ef1 100644 --- a/libcontainer/error.go +++ b/libcontainer/error.go @@ -13,6 +13,7 @@ const ( // Container errors ContainerNotExists + ContainerPausing ContainerPaused ContainerNotStopped ContainerNotRunning @@ -33,6 +34,8 @@ func (c ErrorCode) String() string { return "Id already in use" case InvalidIdFormat: return "Invalid format" + case ContainerPausing: + return "Container pausing" case ContainerPaused: return "Container paused" case ConfigInvalid: diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go index b45ce23e4a5..d47e81915e0 100644 --- a/libcontainer/state_linux.go +++ b/libcontainer/state_linux.go @@ -124,7 +124,7 @@ func (r *runningState) transition(s containerState) error { } r.c.state = s return nil - case *pausedState: + case *pausingState, *pausedState: r.c.state = s return nil case *runningState: @@ -154,7 +154,7 @@ func (i *createdState) status() Status { func (i *createdState) transition(s containerState) error { switch s.(type) { - case *runningState, *pausedState, *stoppedState: + case *runningState, *pausingState, *pausedState, *stoppedState: i.c.state = s return nil case *createdState: @@ -168,7 +168,42 @@ func (i *createdState) destroy() error { return destroy(i.c) } -// pausedState represents a container that is currently pause. It cannot be destroyed in a +// pausingState represents a container that is currently pausing. It cannot be destroyed in a +// pausing state and must transition back to running first. +type pausingState struct { + c *linuxContainer +} + +func (p *pausingState) status() Status { + return Pausing +} + +func (p *pausingState) transition(s containerState) error { + switch s.(type) { + case *pausedState, *runningState, *stoppedState: + p.c.state = s + return nil + case *pausingState: + return nil + } + return newStateTransitionError(p, s) +} + +func (p *pausingState) destroy() error { + t, err := p.c.runType() + if err != nil { + return err + } + if t != Running && t != Created { + if err := p.c.cgroupManager.Freeze(configs.Thawed); err != nil { + return err + } + return destroy(p.c) + } + return newGenericError(fmt.Errorf("container is pausing"), ContainerPausing) +} + +// pausedState represents a container that is currently paused. It cannot be destroyed in a // paused state and must transition back to running first. type pausedState struct { c *linuxContainer diff --git a/libcontainer/state_linux_test.go b/libcontainer/state_linux_test.go index 6ef516b757a..10d745eb40e 100644 --- a/libcontainer/state_linux_test.go +++ b/libcontainer/state_linux_test.go @@ -11,6 +11,7 @@ var states = map[containerState]Status{ &createdState{}: Created, &runningState{}: Running, &restoredState{}: Running, + &pausingState{}: Pausing, &pausedState{}: Paused, &stoppedState{}: Stopped, &loadedState{s: Running}: Running, @@ -67,6 +68,19 @@ func TestStoppedStateTransition(t *testing.T) { ) } +func TestPausingStateTransition(t *testing.T) { + testTransitions( + t, + &pausingState{c: &linuxContainer{}}, + []containerState{ + &pausingState{}, + &pausedState{}, + &runningState{}, + &stoppedState{}, + }, + ) +} + func TestPausedStateTransition(t *testing.T) { testTransitions( t, @@ -96,6 +110,7 @@ func TestRunningStateTransition(t *testing.T) { &runningState{c: &linuxContainer{}}, []containerState{ &stoppedState{}, + &pausingState{}, &pausedState{}, &runningState{}, }, @@ -108,6 +123,7 @@ func TestCreatedStateTransition(t *testing.T) { &createdState{c: &linuxContainer{}}, []containerState{ &stoppedState{}, + &pausingState{}, &pausedState{}, &runningState{}, &createdState{},