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  }