Skip to content

Commit 3b5064a

Browse files
committed
add DestroyIfUnpaused; use it in evictor to avoid ill-timed evictions
1 parent 0e43f13 commit 3b5064a

File tree

8 files changed

+95
-83
lines changed

8 files changed

+95
-83
lines changed

src/lambda/lambda.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ func (linst *LambdaInstance) TrySendError(req *Invocation, statusCode int, msg s
840840

841841
var err error
842842
if sb != nil {
843-
_, err = req.w.Write([]byte(msg+"\nSandbox State:"+sb.DebugString()+"\n"))
843+
_, err = req.w.Write([]byte(msg+"\nSandbox State: "+sb.DebugString()+"\n"))
844844
} else {
845845
_, err = req.w.Write([]byte(msg+"\n"))
846846
}

src/lambda/packagePuller.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"net/http"
1010
"os"
1111
"path/filepath"
12-
"strconv"
1312
"strings"
1413
"sync"
1514
"sync/atomic"
@@ -301,14 +300,8 @@ func (pp *PackagePuller) sandboxInstall(p *Package) (err error) {
301300
}
302301

303302
if resp.StatusCode != http.StatusOK {
304-
// did we run out of memory?
305-
if stat, err := sb.Status(sandbox.StatusMemFailures); err == nil {
306-
if b, err := strconv.ParseBool(stat); err == nil && b {
307-
return fmt.Errorf("ran out of memory while installing %s", p.name)
308-
}
309-
}
310-
311-
return fmt.Errorf("install lambda returned status %d, body '%s'", resp.StatusCode, string(body))
303+
return fmt.Errorf("install lambda returned status %d, Body: '%s', Installer Sandbox State: %s",
304+
resp.StatusCode, string(body), sb.DebugString())
312305
}
313306

314307
if err := json.Unmarshal(body, &p.meta); err != nil {

src/sandbox/api.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ type Sandbox interface {
4747
// error messages
4848
Destroy(reason string)
4949

50+
// The Sandbox may choose to ignore the Destroy request if the
51+
// state is unpaused. Some simple Sandbox implementations may
52+
// also respond to this by always killing the sandbox,
53+
// regardless of current state.
54+
DestroyIfPaused(reason string)
55+
5056
// Make processes in the container non-schedulable
5157
Pause() error
5258

@@ -59,9 +65,6 @@ type Sandbox interface {
5965
// Lookup metadata that Sandbox was initialized with (static over time)
6066
Meta() *SandboxMeta
6167

62-
// Lookup a particular stat (changes over time)
63-
Status(SandboxStatus) (string, error)
64-
6568
// Get output of the runtime; if any
6669
GetRuntimeLog() string
6770

@@ -114,6 +117,7 @@ type SandboxEventType int
114117
const (
115118
EvCreate SandboxEventType = iota
116119
EvDestroy = iota
120+
EvDestroyIgnored = iota
117121
EvPause = iota
118122
EvUnpause = iota
119123
EvFork = iota
@@ -124,9 +128,3 @@ type SandboxEvent struct {
124128
EvType SandboxEventType
125129
SB Sandbox
126130
}
127-
128-
type SandboxStatus int
129-
130-
const (
131-
StatusMemFailures SandboxStatus = iota // boolean
132-
)

src/sandbox/cgroups.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,26 @@ func (cg *Cgroup) WriteString(resource string, val string) {
298298
}
299299
}
300300

301+
func (cg *Cgroup) TryReadIntKV(resource string, key string) (int64, error) {
302+
raw, err := ioutil.ReadFile(cg.ResourcePath(resource))
303+
if err != nil {
304+
return 0, err
305+
}
306+
body := string(raw)
307+
lines := strings.Split(body, "\n")
308+
for i := 0; i <= len(lines); i++ {
309+
parts := strings.Split(lines[i], " ")
310+
if len(parts) == 2 && parts[0] == key {
311+
val, err := strconv.ParseInt(strings.TrimSpace(string(parts[1])), 10, 64)
312+
if err != nil {
313+
return 0, err
314+
}
315+
return val, nil
316+
}
317+
}
318+
return 0, fmt.Errorf("could not find key '%s' in file: %s", key, body)
319+
}
320+
301321
func (cg *Cgroup) TryReadInt(resource string) (int64, error) {
302322
raw, err := ioutil.ReadFile(cg.ResourcePath(resource))
303323
if err != nil {

src/sandbox/docker.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ func (c *DockerContainer) Destroy(reason string) {
192192
}
193193
}
194194

195+
func (c *DockerContainer) DestroyIfPaused(reason string) {
196+
c.Destroy(reason) // we're allowed to implement this by uncondationally destroying
197+
}
198+
195199
// frees all resources associated with the lambda
196200
func (c *DockerContainer) internalDestroy() error {
197201
c.Unpause()
@@ -353,10 +357,6 @@ func waitForServerPipeReady(hostDir string) error {
353357
}
354358
}
355359

356-
func (c *DockerContainer) Status(key SandboxStatus) (string, error) {
357-
return "", STATUS_UNSUPPORTED
358-
}
359-
360360
func (c *DockerContainer) Meta() *SandboxMeta {
361361
return c.meta
362362
}

src/sandbox/evictors.go

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,27 +80,17 @@ func (evictor *SOCKEvictor) Event(evType SandboxEventType, sb Sandbox) {
8080

8181
// move Sandbox to a given queue, removing from previous (if necessary).
8282
// a move to nil is just a delete.
83-
//
84-
// a Sandbox cannot be moved from evicting to another queue (only to nil);
85-
// requests attempting to do so are quietly ignored
8683
func (evictor *SOCKEvictor) move(sb Sandbox, target *list.List) {
8784
// remove from previous queue if necessary
8885
prev := evictor.stateMap[sb.ID()]
8986
if prev != nil {
90-
// you cannot move off evicting to a live queue
91-
if prev.List == evictor.evicting && target != nil {
92-
return
93-
}
94-
9587
prev.List.Remove(prev.Element)
9688
}
9789

9890
// add to new queue
9991
if target != nil {
100-
if target != nil {
101-
element := target.PushBack(sb)
102-
evictor.stateMap[sb.ID()] = &ListLocation{target, element}
103-
}
92+
element := target.PushBack(sb)
93+
evictor.stateMap[sb.ID()] = &ListLocation{target, element}
10494
} else {
10595
delete(evictor.stateMap, sb.ID())
10696
}
@@ -154,7 +144,7 @@ func (evictor *SOCKEvictor) updateState() {
154144
prio += 2
155145
case EvChildExit:
156146
prio -= 2
157-
case EvDestroy:
147+
case EvDestroy, EvDestroyIgnored:
158148
default:
159149
evictor.printf("Unknown event: %v", event.EvType)
160150
}
@@ -174,6 +164,7 @@ func (evictor *SOCKEvictor) updateState() {
174164
if prio >= len(evictor.prioQueues) {
175165
prio = len(evictor.prioQueues) - 1
176166
}
167+
177168
evictor.move(sb, evictor.prioQueues[prio])
178169
}
179170

@@ -183,20 +174,24 @@ func (evictor *SOCKEvictor) updateState() {
183174

184175
// evict whatever SB is at the front of the queue, assumes
185176
// queue is not empty
186-
func (evictor *SOCKEvictor) evictFront(queue *list.List) {
177+
func (evictor *SOCKEvictor) evictFront(queue *list.List, force bool) {
187178
front := queue.Front()
188179
sb := front.Value.(Sandbox)
189180

190181
evictor.printf("Evict Sandbox %v", sb.ID())
182+
evictor.move(sb, evictor.evicting)
191183

192184
// destroy async (we'll know when it's done, because
193185
// we'll see a evDestroy event later on our chan)
194186
go func() {
195187
t := common.T0("evict")
196-
sb.Destroy("evicted")
188+
if force {
189+
sb.Destroy("forced eviction")
190+
} else {
191+
sb.DestroyIfPaused("idle eviction")
192+
}
197193
t.T1()
198194
}()
199-
evictor.move(sb, evictor.evicting)
200195
}
201196

202197
// POLICY: how should we select a victim?
@@ -224,7 +219,7 @@ func (evictor *SOCKEvictor) doEvictions() {
224219

225220
// try evicting the desired number, starting with the paused queue
226221
for evictCount > 0 && evictor.prioQueues[0].Len() > 0 {
227-
evictor.evictFront(evictor.prioQueues[0])
222+
evictor.evictFront(evictor.prioQueues[0], false)
228223
evictCount -= 1
229224
}
230225

@@ -237,12 +232,13 @@ func (evictor *SOCKEvictor) doEvictions() {
237232
if freeSandboxes <= 0 && evictor.evicting.Len() == 0 {
238233
evictor.printf("WARNING! Critically low on memory, so evicting an active Sandbox")
239234
if evictor.prioQueues[1].Len() > 0 {
240-
evictor.evictFront(evictor.prioQueues[1])
235+
evictor.evictFront(evictor.prioQueues[1], true)
241236
}
242237
}
243238

244-
// we never evict from prioQueues[2], because have descendents
245-
// with lower priority that should be evicted first
239+
// we never evict from prioQueues[2+], because those have
240+
// descendents with lower priority that should be evicted
241+
// first
246242
}
247243

248244
func (evictor *SOCKEvictor) Run() {

src/sandbox/safeSandbox.go

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package sandbox
33
// this layer can wrap any sandbox, and provides several (mostly) safety features:
44
// 1. it prevents concurrent calls to Sandbox functions that modify the Sandbox
55
// 2. it automatically destroys unhealthy sandboxes (it is considered unhealthy after returnning any error)
6-
// 3. calls on a destroyed sandbox just return a DEAD_SANDBOX error (no harm is done)
6+
// 3. calls on a destroyed sandbox just return a dead sandbox error (no harm is done)
77
// 4. suppresses Pause calls to already paused Sandboxes, and similar for Unpause calls.
8-
// 5. it traces all calls
8+
// 5. it asyncronously notifies event listeners of state changes
99

1010
import (
1111
"fmt"
@@ -87,13 +87,36 @@ func (sb *safeSandbox) Destroy(reason string) {
8787
}
8888

8989
sb.Sandbox.Destroy(reason)
90-
// TODO: allow message to be passed in (so we can blame cache eviction, for example)
91-
sb.dead = SandboxDeadError(fmt.Sprintf("Sandbox previously killed exlicitly by Destroy(reason=%s) call", reason))
90+
state := "unpaused"
91+
if sb.paused {
92+
state = "paused"
93+
}
94+
sb.dead = SandboxDeadError(fmt.Sprintf("Sandbox previously killed exlicitly by Destroy(reason=%s) call while %s", reason, state))
9295

9396
// let anybody interested know this died
9497
sb.event(EvDestroy)
9598
}
9699

100+
func (sb *safeSandbox) DestroyIfPaused(reason string) {
101+
sb.printf("DestroyIfPaused()")
102+
t := common.T0("DestroyIfPaused()")
103+
defer t.T1()
104+
sb.Mutex.Lock()
105+
defer sb.Mutex.Unlock()
106+
107+
if sb.dead != nil {
108+
return
109+
}
110+
111+
if sb.paused {
112+
sb.Sandbox.Destroy(reason)
113+
sb.dead = SandboxDeadError(fmt.Sprintf("Sandbox previously killed exlicitly by DestroyIfPaused(reason=%s) call", reason))
114+
sb.event(EvDestroy)
115+
} else {
116+
sb.event(EvDestroyIgnored)
117+
}
118+
}
119+
97120
func (sb *safeSandbox) Pause() (err error) {
98121
sb.printf("Pause()")
99122
t := common.T0("Pause()")
@@ -111,8 +134,8 @@ func (sb *safeSandbox) Pause() (err error) {
111134
sb.destroyOnErr("Pause", err)
112135
return err
113136
}
114-
115137
sb.event(EvPause)
138+
116139
sb.paused = true
117140
return nil
118141
}
@@ -130,12 +153,16 @@ func (sb *safeSandbox) Unpause() (err error) {
130153
return nil
131154
}
132155

156+
// we want watchers (e.g., the evictor) to overestimate when
157+
// we're active so that we're less likely to be evicted at a
158+
// bad time, so the Unpause event is sent before the actual op
159+
// and the Pause event is sent after the actual op
160+
sb.event(EvUnpause)
133161
if err := sb.Sandbox.Unpause(); err != nil {
134162
sb.destroyOnErr("Unpause", err)
135163
return err
136164
}
137165

138-
sb.event(EvUnpause)
139166
sb.paused = false
140167
return nil
141168
}
@@ -197,30 +224,12 @@ func (sb *safeSandbox) childExit(child Sandbox) {
197224
}
198225
}
199226

200-
func (sb *safeSandbox) Status(key SandboxStatus) (stat string, err error) {
201-
sb.printf("Status(%d)", key)
202-
t := common.T0("Status()")
203-
defer t.T1()
204-
sb.Mutex.Lock()
205-
defer sb.Mutex.Unlock()
206-
207-
if sb.dead != nil {
208-
return "", sb.dead
209-
}
210-
211-
stat, err = sb.Sandbox.Status(key)
212-
if err != nil && err != STATUS_UNSUPPORTED {
213-
sb.destroyOnErr("Status", err)
214-
}
215-
return stat, err
216-
}
217-
218227
func (sb *safeSandbox) DebugString() string {
219228
sb.Mutex.Lock()
220229
defer sb.Mutex.Unlock()
221230

222231
if sb.dead != nil {
223-
return fmt.Sprintf("SANDBOX %s: DEAD (%s)\n", sb.Sandbox.ID(), sb.dead.Error())
232+
return fmt.Sprintf("SANDBOX %s is DEAD: %s\n", sb.Sandbox.ID(), sb.dead.Error())
224233
}
225234

226235
return sb.Sandbox.DebugString()

src/sandbox/sock.go

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"fmt"
55
"io/ioutil"
66
"log"
7-
"sync/atomic"
7+
"sync/atomic"
88
"net"
99
"net/http"
1010
"net/http/httputil"
@@ -294,11 +294,15 @@ func (container *SOCKContainer) Destroy(reason string) {
294294
container.decCgRefCount()
295295
}
296296

297+
func (c *SOCKContainer) DestroyIfPaused(reason string) {
298+
c.Destroy(reason) // we're allowed to implement this by uncondationally destroying
299+
}
300+
297301
// when the count goes to zero, it means (a) this container and (b)
298302
// all it's descendants are destroyed. Thus, it's safe to release it's
299303
// cgroups, and return the memory allocation to the memPool
300304
func (container *SOCKContainer) decCgRefCount() {
301-
newCount := atomic.AddInt32(&container.cgRefCount, -1)
305+
newCount := atomic.AddInt32(&container.cgRefCount, -1)
302306

303307
container.printf("CG ref count decremented to %d", newCount)
304308
if newCount < 0 {
@@ -429,17 +433,6 @@ func (container *SOCKContainer) fork(dst Sandbox) (err error) {
429433
return nil
430434
}
431435

432-
func (container *SOCKContainer) Status(key SandboxStatus) (string, error) {
433-
switch key {
434-
case StatusMemFailures:
435-
// TODO should we only count max memory or also out of bounds?
436-
status := container.cg.MemoryEvents()
437-
return strconv.FormatBool(status["max"] > 0), nil
438-
default:
439-
return "", STATUS_UNSUPPORTED
440-
}
441-
}
442-
443436
func (container *SOCKContainer) Meta() *SandboxMeta {
444437
return container.meta
445438
}
@@ -490,8 +483,11 @@ func (container *SOCKContainer) DebugString() string {
490483
s += fmt.Sprintf("MEMORY USED: %d of %d MB\n",
491484
container.cg.getMemUsageMB(), container.cg.getMemLimitMB())
492485

493-
s += fmt.Sprintf("MEMORY FAILURES: %d\n",
494-
container.cg.ReadInt("memory.failcnt"))
486+
if kills, err := container.cg.TryReadIntKV("memory.events", "oom_kill"); err == nil {
487+
s += fmt.Sprintf("OOM KILLS: %d\n", kills)
488+
} else {
489+
s += fmt.Sprintf("OOM KILLS: could not read because %d\n", err.Error())
490+
}
495491

496492
return s
497493
}

0 commit comments

Comments
 (0)