@@ -48,6 +48,10 @@ func (c *Controller) Save(ctx context.Context) (*anypb.Any, error) {
4848 ms := c .process .MigrationState ()
4949 state .StdinPort , state .StdoutPort , state .StderrPort = ms .StdinPort , ms .StdoutPort , ms .StderrPort
5050 state .WaitCallID = ms .WaitCallID
51+
52+ // Retain them so a source rollback resume re-opens IO like the destination.
53+ c .stdinPort , c .stdoutPort , c .stderrPort = ms .StdinPort , ms .StdoutPort , ms .StderrPort
54+ c .waitCallID = ms .WaitCallID
5155 }
5256
5357 // Exec processes carry their OCI spec; init processes leave it unset.
@@ -179,8 +183,9 @@ func (c *Controller) Patch(ctx context.Context, containerID string, opts *Create
179183
180184// Resume returns a migrating process to the running state. On the destination
181185// it reattaches the patched process to its live guest counterpart, wires up the
182- // stdio relay, and begins watching for exit. On the source it simply lifts the
183- // freeze that Save applied, since the live process and IO are still intact.
186+ // stdio relay, and begins watching for exit. On the source it re-opens the IO
187+ // the blackout dropped and resumes the relay, since the live process is intact
188+ // but its IO connections are not.
184189// Pass events=nil for an init process, whose exit is reported by its owning
185190// container instead.
186191func (c * Controller ) Resume (ctx context.Context , gcsContainer * gcs.Container , events chan interface {}) error {
@@ -192,19 +197,16 @@ func (c *Controller) Resume(ctx context.Context, gcsContainer *gcs.Container, ev
192197 return nil
193198 }
194199
195- // Source rollback: the live process and IO are intact, so just lift the
196- // freeze that Save applied.
197- if c .state == StateSourceMigrating {
198- c .state = StateRunning
199- return nil
200- }
201-
202- if c .state != StateDestinationMigrating {
200+ if c .state != StateDestinationMigrating && c .state != StateSourceMigrating {
203201 return fmt .Errorf ("process %q in container %q is in state %s; cannot resume: %w" , c .execID , c .containerID , c .state , errdefs .ErrFailedPrecondition )
204202 }
205203
206- // Reopen the live process on its preserved IO ports and wait id.
207- gcsProc , err := gcsContainer .OpenProcessWithIO (ctx , uint32 (c .processID ), c .stdinPort , c .stdoutPort , c .stderrPort , c .waitCallID )
204+ // Flag to determine if the resume is happening on destination.
205+ isDestination := c .state == StateDestinationMigrating
206+
207+ // Reopen the process on its preserved IO ports and wait id. A source rollback
208+ // reuses the still-outstanding wait, so it does not start a second one.
209+ gcsProc , err := gcsContainer .OpenProcessWithIO (ctx , uint32 (c .processID ), c .stdinPort , c .stdoutPort , c .stderrPort , c .waitCallID , isDestination )
208210 if err != nil {
209211 return fmt .Errorf ("open gcs process pid %d in container %q: %w" , c .processID , c .containerID , err )
210212 }
@@ -223,9 +225,10 @@ func (c *Controller) Resume(ctx context.Context, gcsContainer *gcs.Container, ev
223225 // Ports are single-use; clear them now that IO is reattached.
224226 c .stdinPort , c .stdoutPort , c .stderrPort = 0 , 0 , 0
225227
226- // Watch for exit in the background, mirroring a freshly started process.
227- go c .handleProcessExit (ctx , execCmd , events )
228+ // The destination owns exit reporting; a source rollback leaves that to the
229+ // watcher from Start, so this handler only drains the re-attached relay.
230+ go c .handleProcessExit (ctx , execCmd , events , isDestination )
228231
229- log .G (ctx ).WithField (logfields .ProcessID , c .processID ).Debug ("resumed migrated process on destination " )
232+ log .G (ctx ).WithField (logfields .ProcessID , c .processID ).Debug ("resumed migrated process" )
230233 return nil
231234}
0 commit comments