stub: bound Configure() wait in Start() and break connClosed() deadlock#298
stub: bound Configure() wait in Start() and break connClosed() deadlock#298wevans-ant wants to merge 1 commit into
Conversation
|
@klihub, this picks up where #238 left off. It keeps the Would be interested to know why you dropped the PR, let me know if there's something I am missing. |
3899911 to
147e2be
Compare
@wevans-ant Thanks for picking this up and working on it. About #238, IIRC it triggered a data read/write race in the tests. I don't recall the details, but it might have been on stub.registrationTimeout. If it was then your PR now avoids it, IIUC by sampling registrationTimeout before it could get written in Configure(). Although I think this has the side-effect that if the registration timeout has been altered by configuration on the runtime/NRI server side then the stub/client will not use this timeout but the previous locally known one instead (which should be the default one in the beginning). |
147e2be to
c574df4
Compare
Start() holds stub.Lock() for its entire body and blocks on <-cfgErrC waiting for the runtime's Configure() callback. If the runtime accepts RegisterPlugin but never sends Configure() (observed when a plugin and containerd race during node boot), Start() blocks forever holding the lock. When the connection later closes, the ttrpc OnClose handler connClosed() tries to take the same lock and deadlocks, so the plugin never observes the disconnect and cannot recover. Fix by: - replacing the naked <-cfgErrC receive with a select that also honours ctx cancellation and a registrationTimeout-bounded timer; - having connClosed() do a non-blocking send of a ttrpc.ErrClosed-wrapped error on cfgErrC before taking the lock, so a blocked Start() is woken and releases the lock; - making Configure()'s deferred cfgErrC send non-blocking so it cannot wedge if Start() has already given up or connClosed() has already filled the buffer. To keep this race-clean under -race: - snapshot stub.registrationTimeout into a local before any goroutines are running, since Configure() rewrites that field without the lock; - create srvErrC/cfgErrC before ttrpc.NewClient spawns the goroutine whose OnClose reads cfgErrC without the lock (this is the race that containerd#238's test surfaced). Add two adaptation-suite regression specs: one that lets Register succeed and blocks the plugin's Configure handler so cfgErrC is never written, then has the runtime time out and close the connection (deterministic deadlock repro); and one that has the runtime drop the connection during registration (surfaces the cfgErrC field race under -race). Supersedes containerd#238. Signed-off-by: Chris Evans <wevans@anthropic.com>
c574df4 to
78ea274
Compare
|
#238's failing run's logs have expired, but I reproduced its 1 ns registration-timeout test under
with the artificially lowered timeout, the connection dies before I've moved the
The timer bounds the wait for the |
stub.Start()holdsstub.Lock()for its whole body and then does an unbounded<-stub.cfgErrCwaiting for the runtime'sConfigure()callback. If the runtime acceptsRegisterPluginbut never sendsConfigure()— which can happen when a plugin and containerd race during node boot —Start()blocks forever with the lock held. When the connection later closes, the ttrpcOnClosehandlerconnClosed()tries to take the same lock and deadlocks, so the plugin never observes the disconnect,WithOnClosenever fires, and the plugin cannot recover.This change:
<-stub.cfgErrCreceive with aselectthat also honoursctxcancellation and aregistrationTimeout-bounded timer, soStart()cannot block indefinitely;connClosed()do a non-blocking send of attrpc.ErrClosed-wrapped error ontocfgErrCbefore taking the lock, so a blockedStart()is woken and releases the lock (this is the mechanism from stub: cancel Start() for early connection loss. #238);Configure()'s deferredcfgErrCsend non-blocking so it cannot wedge ifStart()has already given up orconnClosed()has already filled the buffer.The new adaptation-suite spec reproduces the deadlock deterministically: it lets
Registersucceed, blocks the plugin'sConfigurehandler socfgErrCis never written, then has the runtime time out theConfigureRPC and close the connection. Without this changeStart()never returns and the spec fails at the 3 s guard; with it,Start()returns a wrappedttrpc.ErrClosedin ~200 ms.Supersedes #238.