@@ -16,6 +16,7 @@ import (
1616 rt "runtime"
1717 "strconv"
1818 "strings"
19+ "sync"
1920 "time"
2021
2122 "github.com/docker/docker/api/types/container"
@@ -50,6 +51,81 @@ const (
5051 LabelValueTrue = "true"
5152)
5253
54+ // https://github.com/moby/moby/blob/e77ff99ede5ee5952b3a9227863552ae6e5b6fb1/pkg/jsonmessage/jsonmessage.go#L144
55+ // All fields are optional.
56+ type PullMessage struct {
57+ Id string `json:"id"` // layer id
58+ Status string `json:"status"`
59+ ProgressDetail struct {
60+ Current uint64 `json:"current"` // bytes
61+ Total uint64 `json:"total"` // bytes
62+ } `json:"progressDetail"`
63+ ErrorDetail struct {
64+ Message string `json:"message"`
65+ } `json:"errorDetail"`
66+ }
67+
68+ type PullTracker struct {
69+ mu sync.RWMutex
70+ layers map [string ]LayerPullProgress
71+ }
72+
73+ func newPullTracker () * PullTracker {
74+ return & PullTracker {layers : make (map [string ]LayerPullProgress )}
75+ }
76+
77+ func (t * PullTracker ) Update (msg PullMessage ) {
78+ if msg .Id == "" {
79+ return
80+ }
81+ t .mu .Lock ()
82+ defer t .mu .Unlock ()
83+ layer := t .layers [msg .Id ]
84+ switch msg .Status {
85+ case "Pulling fs layer" , "Waiting" , "Verifying Checksum" , "Already exists" :
86+ // no bytes to update, just track status
87+ case "Downloading" :
88+ layer .DownloadedBytes = msg .ProgressDetail .Current
89+ layer .TotalBytes = msg .ProgressDetail .Total
90+ case "Download complete" :
91+ layer .DownloadedBytes = layer .TotalBytes
92+ case "Extracting" :
93+ layer .ExtractedBytes = msg .ProgressDetail .Current
94+ layer .DownloadedBytes = msg .ProgressDetail .Total
95+ layer .TotalBytes = msg .ProgressDetail .Total
96+ case "Pull complete" :
97+ layer .ExtractedBytes = layer .TotalBytes
98+ layer .DownloadedBytes = layer .TotalBytes
99+ default :
100+ // Non-layer events, such as {"status":"Pulling from library/python","id":"3.11"}
101+ return
102+ }
103+ layer .Status = msg .Status
104+ t .layers [msg .Id ] = layer
105+ }
106+
107+ func (t * PullTracker ) Progress () * ImagePullProgress {
108+ t .mu .RLock ()
109+ defer t .mu .RUnlock ()
110+ if len (t .layers ) == 0 {
111+ return nil
112+ }
113+ p := ImagePullProgress {
114+ IsTotalBytesFinal : true ,
115+ Layers : make (map [string ]LayerPullProgress , len (t .layers )),
116+ }
117+ for id , l := range t .layers {
118+ if l .TotalBytes == 0 && l .Status != "Already exists" && l .Status != "Pull complete" {
119+ p .IsTotalBytesFinal = false
120+ }
121+ p .DownloadedBytes += l .DownloadedBytes
122+ p .ExtractedBytes += l .ExtractedBytes
123+ p .TotalBytes += l .TotalBytes
124+ p .Layers [id ] = l
125+ }
126+ return & p
127+ }
128+
53129type DockerRunner struct {
54130 client * docker.Client
55131 dockerParams DockerParameters
@@ -239,6 +315,7 @@ func (d *DockerRunner) TaskInfo(taskID string) TaskInfo {
239315 ContainerName : task .containerName ,
240316 ContainerID : task .containerID ,
241317 GpuIDs : task .gpuIDs ,
318+ ImagePullProgress : task .pullTracker .Progress (),
242319 }
243320}
244321
@@ -350,7 +427,7 @@ func (d *DockerRunner) Run(ctx context.Context, taskID string) error {
350427 // Although it's called "runner dir", we also use it for shim task-related data.
351428 // Maybe we should rename it to "task dir" (including the `/root/.dstack/runners` dir on the host).
352429 pullLogPath := filepath .Join (runnerDir , "pull.log" )
353- if err = pullImage (pullCtx , d .client , cfg , pullLogPath ); err != nil {
430+ if err = pullImage (pullCtx , d .client , cfg , pullLogPath , task . pullTracker ); err != nil {
354431 errMessage := fmt .Sprintf ("pullImage error: %s" , err .Error ())
355432 log .Error (ctx , errMessage )
356433 task .SetStatusTerminated (string (types .TerminationReasonCreatingContainerError ), errMessage )
@@ -670,7 +747,7 @@ func mountDisk(ctx context.Context, deviceName, mountPoint string, fsRootPerms o
670747 return nil
671748}
672749
673- func pullImage (ctx context.Context , client docker.APIClient , taskConfig TaskConfig , logPath string ) error {
750+ func pullImage (ctx context.Context , client docker.APIClient , taskConfig TaskConfig , logPath string , tracker * PullTracker ) error {
674751 if ! strings .Contains (taskConfig .ImageName , ":" ) {
675752 taskConfig .ImageName += ":latest"
676753 }
@@ -710,26 +787,8 @@ func pullImage(ctx context.Context, client docker.APIClient, taskConfig TaskConf
710787
711788 teeReader := io .TeeReader (reader , logFile )
712789
713- current := make (map [string ]uint )
714- total := make (map [string ]uint )
715-
716790 // dockerd reports pulling progress as a stream of JSON Lines. The format of records is not documented in the API documentation,
717791 // although it's occasionally mentioned, e.g., https://docs.docker.com/reference/api/engine/version-history/#v148-api-changes
718-
719- // https://github.com/moby/moby/blob/e77ff99ede5ee5952b3a9227863552ae6e5b6fb1/pkg/jsonmessage/jsonmessage.go#L144
720- // All fields are optional
721- type PullMessage struct {
722- Id string `json:"id"` // layer id
723- Status string `json:"status"`
724- ProgressDetail struct {
725- Current uint `json:"current"` // bytes
726- Total uint `json:"total"` // bytes
727- } `json:"progressDetail"`
728- ErrorDetail struct {
729- Message string `json:"message"`
730- } `json:"errorDetail"`
731- }
732-
733792 var pullCompleted bool
734793 pullErrors := make ([]string , 0 )
735794
@@ -740,13 +799,7 @@ func pullImage(ctx context.Context, client docker.APIClient, taskConfig TaskConf
740799 if err := json .Unmarshal (line , & pullMessage ); err != nil {
741800 continue
742801 }
743- if pullMessage .Status == "Downloading" {
744- current [pullMessage .Id ] = pullMessage .ProgressDetail .Current
745- total [pullMessage .Id ] = pullMessage .ProgressDetail .Total
746- }
747- if pullMessage .Status == "Download complete" {
748- current [pullMessage .Id ] = total [pullMessage .Id ]
749- }
802+ tracker .Update (pullMessage )
750803 if pullMessage .ErrorDetail .Message != "" {
751804 log .Error (ctx , "error pulling image" , "name" , taskConfig .ImageName , "err" , pullMessage .ErrorDetail .Message )
752805 pullErrors = append (pullErrors , pullMessage .ErrorDetail .Message )
@@ -764,13 +817,10 @@ func pullImage(ctx context.Context, client docker.APIClient, taskConfig TaskConf
764817 }
765818
766819 duration := time .Since (startTime )
767- var currentBytes uint
768- var totalBytes uint
769- for _ , v := range current {
770- currentBytes += v
771- }
772- for _ , v := range total {
773- totalBytes += v
820+ p := tracker .Progress ()
821+ var currentBytes , totalBytes uint64
822+ if p != nil {
823+ currentBytes , totalBytes = p .DownloadedBytes , p .TotalBytes
774824 }
775825 speed := bytesize .New (float64 (currentBytes ) / duration .Seconds ())
776826
0 commit comments