-
Notifications
You must be signed in to change notification settings - Fork 403
/
image_cache.go
270 lines (221 loc) · 6.48 KB
/
image_cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package docker
import (
"container/list"
"sync"
)
// ImageCacher is an image tracker for docker driver. It consists
// of a LRU cache and a reference count map. It keeps of both in-use
// and not in-use images and if the total size exceeds the configured
// limit, then it notifies its consumer. The consumer is expected to
// use IsMaxCapacity()/GetNotifier() functions to keep track of the
// capacity state and is expected to use Pop() function to remove a
// least recently used image from the cache. ImageCacher provides
// Update() to add/update the LRU cache and MarkBusy()/MarkFree()
// function pair to mark/unmark a specific image (reference count)
// in use.
type CachedImage struct {
ID string // Image Cache key
ParentID string
RepoTags []string // RepoTags are used to match special/status images that are exempt from image cache
Size uint64
}
type ImageCacherStats struct {
BusyImgTotalSize uint64
BusyImgCount uint64
IdleImgTotalSize uint64
IdleImgCount uint64
MaxImgTotalSize uint64
}
type ImageCacher interface {
// IsMaxCapacity returns true if total size of all images exceeds the limit
// and if there's an image in LRU cache that can be removed.
IsMaxCapacity() bool
// GetNotifier returns a channel that can be monitored. The channel will return
// data every time IsMaxCapacity() flips from false to true state.
GetNotifier() <-chan struct{}
// Removes an image from the LRU cache if cache is not empty
Pop() *CachedImage
// Update adds an image to the LRU cache if the image is not marked in-use
Update(img *CachedImage)
// Mark/Unmark an image in-use. If an image is in-use, it will
// not be a candidate in LRU. When the reference count of the
// image drops to zero (via MarkFree() calls), the image will
// be added back to LRU.
MarkBusy(img *CachedImage)
MarkFree(img *CachedImage)
// Stats Monitoring
GetStats() *ImageCacherStats
}
type imageCacher struct {
// max total size of images that are in-use or in LRU cache
maxSize uint64
// image names/tags that are not eligible for image cacher
blacklistTags map[string]struct{}
// notification channel for/when image cacher is over capacity
// and has an LRU candidate
notifier chan struct{}
// big lock for both busy and lru elements below
lock sync.Mutex
// LRU for images that are not in-use
lruSize uint64
lruList *list.List
lruMap map[string]*list.Element
// reference count of images that are in-use
busySize uint64
busyRef map[string]uint64
}
func NewImageCache(exemptTags []string, maxSize uint64) ImageCacher {
c := imageCacher{
maxSize: maxSize,
blacklistTags: make(map[string]struct{}),
notifier: make(chan struct{}, 1),
lruList: list.New(),
lruMap: make(map[string]*list.Element),
busyRef: make(map[string]uint64),
}
for _, tag := range exemptTags {
c.blacklistTags[tag] = struct{}{}
}
return &c
}
// isEligible returns true if the image is eligible to be managed
// by image cacher.
func (c *imageCacher) isEligible(img *CachedImage) bool {
for _, tag := range img.RepoTags {
if _, ok := c.blacklistTags[tag]; ok {
return false
}
}
return true
}
// addLRULocked performs a classic LRU add operation
func (c *imageCacher) addLRULocked(img *CachedImage) {
ee, ok := c.lruMap[img.ID]
if ok {
c.lruList.MoveToFront(ee)
} else {
c.lruSize += img.Size
c.lruMap[img.ID] = c.lruList.PushFront(img)
}
}
// addLRULocked performs a classic LRU remove operation
func (c *imageCacher) rmLRULocked(img *CachedImage) {
ee, ok := c.lruMap[img.ID]
if ok {
c.lruList.Remove(ee)
delete(c.lruMap, img.ID)
c.lruSize -= ee.Value.(*CachedImage).Size
}
}
// addBusyLocked updates an image in the in-use list and returns true if
// a new insertion to in-use list was performed.
func (c *imageCacher) addBusyLocked(img *CachedImage) bool {
if ee, ok := c.busyRef[img.ID]; ok {
c.busyRef[img.ID] = 1 + ee
return false
}
c.busyRef[img.ID] = 1
c.busySize += img.Size
return true
}
// rmBusyLocked updates an image in the in-use list and returns true if
// the image is removed from the in-use list.
func (c *imageCacher) rmBusyLocked(img *CachedImage) bool {
if ee, ok := c.busyRef[img.ID]; ok {
if ee > 1 {
c.busyRef[img.ID] = ee - 1
return false
}
delete(c.busyRef, img.ID)
c.busySize -= img.Size
return true
}
return false
}
// sendNotify tries to wake up any pending listener on notify channel
func (c *imageCacher) sendNotify() {
select {
case c.notifier <- struct{}{}:
default:
}
}
// We compare both busy + lru size against max. However, we also check if lru is not empty.
// This is because there's no point to show over capacity if Pop() is going to return nil.
func (c *imageCacher) isMaxCapacityLocked() bool {
return (c.lruSize > 0) && ((c.lruSize + c.busySize) >= c.maxSize)
}
func (c *imageCacher) GetStats() *ImageCacherStats {
stats := &ImageCacherStats{
MaxImgTotalSize: c.maxSize,
}
c.lock.Lock()
stats.BusyImgTotalSize = c.busySize
stats.BusyImgCount = uint64(len(c.busyRef))
stats.IdleImgTotalSize = c.lruSize
stats.IdleImgCount = uint64(len(c.lruMap))
c.lock.Unlock()
return stats
}
func (c *imageCacher) GetNotifier() <-chan struct{} {
return c.notifier
}
func (c *imageCacher) IsMaxCapacity() bool {
c.lock.Lock()
defer c.lock.Unlock()
return c.isMaxCapacityLocked()
}
// Update adds an image to LRU if the image is not in-use.
func (c *imageCacher) Update(img *CachedImage) {
if !c.isEligible(img) {
return
}
c.lock.Lock()
if _, ok := c.busyRef[img.ID]; !ok {
c.addLRULocked(img)
if c.isMaxCapacityLocked() {
defer c.sendNotify()
}
}
c.lock.Unlock()
}
// Pop removes and returns an image from LRU if LRU is not empty
func (c *imageCacher) Pop() *CachedImage {
c.lock.Lock()
defer c.lock.Unlock()
item := c.lruList.Back()
if item == nil {
return nil
}
img := item.Value.(*CachedImage)
c.rmLRULocked(img)
return img
}
// MarkBusy marks an image as in-use and removes it from LRU
func (c *imageCacher) MarkBusy(img *CachedImage) {
if !c.isEligible(img) {
return
}
c.lock.Lock()
if c.addBusyLocked(img) {
c.rmLRULocked(img)
if c.isMaxCapacityLocked() {
defer c.sendNotify()
}
}
c.lock.Unlock()
}
// MarkFree marks an image as not in-use and if in-use reference count
// for the image becomes zero, then MarkFree adds the image to the LRU
func (c *imageCacher) MarkFree(img *CachedImage) {
if !c.isEligible(img) {
return
}
c.lock.Lock()
if c.rmBusyLocked(img) {
c.addLRULocked(img)
if c.isMaxCapacityLocked() {
defer c.sendNotify()
}
}
c.lock.Unlock()
}