github.com/anuvu/nomad@v0.8.7-atom1/client/driver/docker_progress.go (about)

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