github.com/ilhicas/nomad@v0.11.8/drivers/docker/progress.go (about) 1 package docker 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/docker/docker/pkg/jsonmessage" 14 units "github.com/docker/go-units" 15 ) 16 17 const ( 18 // dockerImageProgressReportInterval is the default value set in the 19 // imageProgressManager when newImageProgressManager is called 20 dockerImageProgressReportInterval = 10 * time.Second 21 22 // dockerImageSlowProgressReportInterval is the default value set in the 23 // imageProgressManager when newImageProgressManager is called 24 dockerImageSlowProgressReportInterval = 2 * time.Minute 25 ) 26 27 // layerProgress tracks the state and downloaded bytes of a single layer within 28 // a docker image 29 type layerProgress struct { 30 id string 31 status layerProgressStatus 32 currentBytes int64 33 totalBytes int64 34 } 35 36 type layerProgressStatus int 37 38 const ( 39 layerProgressStatusUnknown layerProgressStatus = iota 40 layerProgressStatusStarting 41 layerProgressStatusWaiting 42 layerProgressStatusDownloading 43 layerProgressStatusVerifying 44 layerProgressStatusDownloaded 45 layerProgressStatusExtracting 46 layerProgressStatusComplete 47 layerProgressStatusExists 48 ) 49 50 func lpsFromString(status string) layerProgressStatus { 51 switch status { 52 case "Pulling fs layer": 53 return layerProgressStatusStarting 54 case "Waiting": 55 return layerProgressStatusWaiting 56 case "Downloading": 57 return layerProgressStatusDownloading 58 case "Verifying Checksum": 59 return layerProgressStatusVerifying 60 case "Download complete": 61 return layerProgressStatusDownloaded 62 case "Extracting": 63 return layerProgressStatusExtracting 64 case "Pull complete": 65 return layerProgressStatusComplete 66 case "Already exists": 67 return layerProgressStatusExists 68 default: 69 return layerProgressStatusUnknown 70 } 71 } 72 73 // imageProgress tracks the status of each child layer as its pulled from a 74 // docker image repo 75 type imageProgress struct { 76 sync.RWMutex 77 lastMessage *jsonmessage.JSONMessage 78 timestamp time.Time 79 layers map[string]*layerProgress 80 pullStart time.Time 81 } 82 83 // get returns a status message and the timestamp of the last status update 84 func (p *imageProgress) get() (string, time.Time) { 85 p.RLock() 86 defer p.RUnlock() 87 88 if p.lastMessage == nil { 89 return "No progress", p.timestamp 90 } 91 92 var pulled, pulling, waiting int 93 for _, l := range p.layers { 94 switch { 95 case l.status == layerProgressStatusStarting || 96 l.status == layerProgressStatusWaiting: 97 waiting++ 98 case l.status == layerProgressStatusDownloading || 99 l.status == layerProgressStatusVerifying: 100 pulling++ 101 case l.status >= layerProgressStatusDownloaded: 102 pulled++ 103 } 104 } 105 106 elapsed := time.Now().Sub(p.pullStart) 107 cur := p.currentBytes() 108 total := p.totalBytes() 109 var est int64 110 if cur != 0 { 111 est = (elapsed.Nanoseconds() / cur * total) - elapsed.Nanoseconds() 112 } 113 114 var msg strings.Builder 115 fmt.Fprintf(&msg, "Pulled %d/%d (%s/%s) layers: %d waiting/%d pulling", 116 pulled, len(p.layers), units.BytesSize(float64(cur)), units.BytesSize(float64(total)), 117 waiting, pulling) 118 119 if est > 0 { 120 fmt.Fprintf(&msg, " - est %.1fs remaining", time.Duration(est).Seconds()) 121 } 122 return msg.String(), p.timestamp 123 } 124 125 // set takes a status message received from the docker engine api during an image 126 // pull and updates the status of the corresponding layer 127 func (p *imageProgress) set(msg *jsonmessage.JSONMessage) { 128 p.Lock() 129 defer p.Unlock() 130 131 p.lastMessage = msg 132 p.timestamp = time.Now() 133 134 lps := lpsFromString(msg.Status) 135 if lps == layerProgressStatusUnknown { 136 return 137 } 138 139 layer, ok := p.layers[msg.ID] 140 if !ok { 141 layer = &layerProgress{id: msg.ID} 142 p.layers[msg.ID] = layer 143 } 144 layer.status = lps 145 if msg.Progress != nil && lps == layerProgressStatusDownloading { 146 layer.currentBytes = msg.Progress.Current 147 layer.totalBytes = msg.Progress.Total 148 } else if lps == layerProgressStatusDownloaded { 149 layer.currentBytes = layer.totalBytes 150 } 151 } 152 153 // currentBytes iterates through all image layers and sums the total of 154 // current bytes. The caller is responsible for acquiring a read lock on the 155 // imageProgress struct 156 func (p *imageProgress) currentBytes() int64 { 157 var b int64 158 for _, l := range p.layers { 159 b += l.currentBytes 160 } 161 return b 162 } 163 164 // totalBytes iterates through all image layers and sums the total of 165 // total bytes. The caller is responsible for acquiring a read lock on the 166 // imageProgress struct 167 func (p *imageProgress) totalBytes() int64 { 168 var b int64 169 for _, l := range p.layers { 170 b += l.totalBytes 171 } 172 return b 173 } 174 175 // progressReporterFunc defines the method for handling inactivity and report 176 // events from the imageProgressManager. The image name, current status message 177 // and timestamp of last received status update are passed in. 178 type progressReporterFunc func(image string, msg string, timestamp time.Time) 179 180 // imageProgressManager tracks the progress of pulling a docker image from an 181 // image repository. 182 // It also implemented the io.Writer interface so as to be passed to the docker 183 // client pull image method in order to receive status updates from the docker 184 // engine api. 185 type imageProgressManager struct { 186 imageProgress *imageProgress 187 image string 188 activityDeadline time.Duration 189 inactivityFunc progressReporterFunc 190 reportInterval time.Duration 191 reporter progressReporterFunc 192 slowReportInterval time.Duration 193 slowReporter progressReporterFunc 194 lastSlowReport time.Time 195 cancel context.CancelFunc 196 stopCh chan struct{} 197 buf bytes.Buffer 198 } 199 200 func newImageProgressManager( 201 image string, cancel context.CancelFunc, 202 pullActivityTimeout time.Duration, inactivityFunc, reporter, slowReporter progressReporterFunc) *imageProgressManager { 203 204 pm := &imageProgressManager{ 205 image: image, 206 activityDeadline: pullActivityTimeout, 207 inactivityFunc: inactivityFunc, 208 reportInterval: dockerImageProgressReportInterval, 209 reporter: reporter, 210 slowReportInterval: dockerImageSlowProgressReportInterval, 211 slowReporter: slowReporter, 212 imageProgress: &imageProgress{ 213 timestamp: time.Now(), 214 layers: make(map[string]*layerProgress), 215 }, 216 cancel: cancel, 217 stopCh: make(chan struct{}), 218 } 219 220 pm.start() 221 return pm 222 } 223 224 // start intiates the ticker to trigger the inactivity and reporter handlers 225 func (pm *imageProgressManager) start() { 226 now := time.Now() 227 pm.imageProgress.pullStart = now 228 pm.lastSlowReport = now 229 go func() { 230 ticker := time.NewTicker(dockerImageProgressReportInterval) 231 for { 232 select { 233 case <-ticker.C: 234 msg, lastStatusTime := pm.imageProgress.get() 235 t := time.Now() 236 if t.Sub(lastStatusTime) > pm.activityDeadline { 237 pm.inactivityFunc(pm.image, msg, lastStatusTime) 238 pm.cancel() 239 return 240 } 241 if t.Sub(pm.lastSlowReport) > pm.slowReportInterval { 242 pm.slowReporter(pm.image, msg, lastStatusTime) 243 pm.lastSlowReport = t 244 } 245 pm.reporter(pm.image, msg, lastStatusTime) 246 case <-pm.stopCh: 247 return 248 } 249 } 250 }() 251 } 252 253 func (pm *imageProgressManager) stop() { 254 close(pm.stopCh) 255 } 256 257 func (pm *imageProgressManager) Write(p []byte) (n int, err error) { 258 n, err = pm.buf.Write(p) 259 var msg jsonmessage.JSONMessage 260 261 for { 262 line, err := pm.buf.ReadBytes('\n') 263 if err == io.EOF { 264 // Partial write of line; push back onto buffer and break until full line 265 pm.buf.Write(line) 266 break 267 } 268 if err != nil { 269 return n, err 270 } 271 err = json.Unmarshal(line, &msg) 272 if err != nil { 273 return n, err 274 } 275 276 if msg.Error != nil { 277 // error received from the docker engine api 278 return n, msg.Error 279 } 280 281 pm.imageProgress.set(&msg) 282 } 283 284 return 285 }