@@ -351,20 +351,26 @@ func TestReadAllTrackingFiles_NoTrackingDirReturnsEmpty(t *testing.T) {
351351
352352// fakeNVMeSubsystem is a minimal hand-rolled fake for nvme.NVMeSubsystemInterface: no gomock
353353// mock exists for this interface under mocks/mock_utils/nvme, only for NVMeInterface. Any method
354- // besides Disconnect is intentionally left unimplemented (nil-embedded) since
354+ // besides Disconnect and GetNamespaceCount is intentionally left unimplemented (nil-embedded) since
355355// disconnectNVMeSubsystemIfNeeded never calls them; using any of them would panic, which is the
356356// desired failure mode if the code under test changes to call something unexpected.
357357type fakeNVMeSubsystem struct {
358358 nvme.NVMeSubsystemInterface
359359 disconnectErr error
360360 disconnectCalls int
361+ hostNsCount int
362+ hostNsCountErr error
361363}
362364
363365func (f * fakeNVMeSubsystem ) Disconnect (_ context.Context ) error {
364366 f .disconnectCalls ++
365367 return f .disconnectErr
366368}
367369
370+ func (f * fakeNVMeSubsystem ) GetNamespaceCount (_ context.Context ) (int , error ) {
371+ return f .hostNsCount , f .hostNsCountErr
372+ }
373+
368374// withCleanPublishedNVMeSessions snapshots and restores the package-level publishedNVMeSessions
369375// global so this test's seeding doesn't bleed into other tests in the package.
370376func withCleanPublishedNVMeSessions (t * testing.T ) {
@@ -373,59 +379,56 @@ func withCleanPublishedNVMeSessions(t *testing.T) {
373379 t .Cleanup (func () { publishedNVMeSessions = original })
374380}
375381
376- func TestDisconnectNVMeSubsystemIfNeeded_NoNamespaces_DisconnectsRegardlessOfFlag (t * testing.T ) {
377- withCleanPublishedNVMeSessions (t )
378- core , _ := newTestCore (t )
379- pi := samplePublishInfo (NVMe )
380- fakeSubsys := & fakeNVMeSubsystem {}
381-
382- err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi , false )
383-
384- require .NoError (t , err )
385- assert .Equal (t , 1 , fakeSubsys .disconnectCalls )
386- }
387-
388- func TestDisconnectNVMeSubsystemIfNeeded_NamespacesPresent_DisconnectFlagFalse_NoDisconnect (t * testing.T ) {
382+ func TestDisconnectNVMeSubsystemIfNeeded_PublishedNamespacePresent_NoDisconnect (t * testing.T ) {
389383 withCleanPublishedNVMeSessions (t )
390384 core , _ := newTestCore (t , WithNVMeSelfHealingInterval (5 * time .Second ))
391385 pi := samplePublishInfo (NVMe )
392386 publishedNVMeSessions .AddNVMeSession (nvme.NVMeSubsystem {NQN : pi .NVMeSubsystemNQN }, nil )
393387 publishedNVMeSessions .AddNamespaceToSession (pi .NVMeSubsystemNQN , "ns-1" )
394- fakeSubsys := & fakeNVMeSubsystem {}
388+ fakeSubsys := & fakeNVMeSubsystem {hostNsCount : 1 }
395389
396- err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi , false )
390+ err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi )
397391
398392 require .NoError (t , err )
399393 assert .Equal (t , 0 , fakeSubsys .disconnectCalls )
400394}
401395
402- func TestDisconnectNVMeSubsystemIfNeeded_NamespacesPresent_SelfHealingDisabled_NoDisconnect (t * testing.T ) {
403- withCleanPublishedNVMeSessions (t )
404- // nvmeSelfHealingInterval defaults to zero (disabled) when not set via WithNVMeSelfHealingInterval.
405- core , _ := newTestCore (t )
406- pi := samplePublishInfo (NVMe )
407- publishedNVMeSessions .AddNVMeSession (nvme.NVMeSubsystem {NQN : pi .NVMeSubsystemNQN }, nil )
408- publishedNVMeSessions .AddNamespaceToSession (pi .NVMeSubsystemNQN , "ns-1" )
409- fakeSubsys := & fakeNVMeSubsystem {}
410-
411- err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi , true )
412-
413- require .NoError (t , err )
414- assert .Equal (t , 0 , fakeSubsys .disconnectCalls , "self-healing disabled must gate the disconnect hint even if disconnect=true" )
415- }
396+ // Once no published sessions remain, the host namespace count is the tie-breaker: a count above one
397+ // means a concurrent NodeStage already attached a namespace it hasn't recorded a session for yet, so
398+ // disconnecting would pull that device out from under the new pod. Any other outcome, including an
399+ // unreadable count, falls through to the disconnect.
400+ func TestDisconnectNVMeSubsystemIfNeeded_NoPublishedNamespaces_HostCountDecides (t * testing.T ) {
401+ tests := map [string ]struct {
402+ hostNsCount int
403+ hostNsCountErr error
404+ wantDisconnectCalls int
405+ }{
406+ "another namespace still attached on host" : {hostNsCount : 2 , wantDisconnectCalls : 0 },
407+ "several namespaces still attached" : {hostNsCount : 5 , wantDisconnectCalls : 0 },
408+ "only our own namespace attached" : {hostNsCount : 1 , wantDisconnectCalls : 1 },
409+ "no namespaces attached" : {hostNsCount : 0 , wantDisconnectCalls : 1 },
410+ "host count unreadable" : {
411+ hostNsCountErr : errors .New ("failed to read namespace count" ),
412+ wantDisconnectCalls : 1 ,
413+ },
414+ }
416415
417- func TestDisconnectNVMeSubsystemIfNeeded_NamespacesPresent_SelfHealingEnabledAndDisconnect_Disconnects (t * testing.T ) {
418- withCleanPublishedNVMeSessions (t )
419- core , _ := newTestCore (t , WithNVMeSelfHealingInterval (5 * time .Second ))
420- pi := samplePublishInfo (NVMe )
421- publishedNVMeSessions .AddNVMeSession (nvme.NVMeSubsystem {NQN : pi .NVMeSubsystemNQN }, nil )
422- publishedNVMeSessions .AddNamespaceToSession (pi .NVMeSubsystemNQN , "ns-1" )
423- fakeSubsys := & fakeNVMeSubsystem {}
416+ for name , test := range tests {
417+ t .Run (name , func (t * testing.T ) {
418+ withCleanPublishedNVMeSessions (t )
419+ core , _ := newTestCore (t )
420+ pi := samplePublishInfo (NVMe )
421+ fakeSubsys := & fakeNVMeSubsystem {
422+ hostNsCount : test .hostNsCount ,
423+ hostNsCountErr : test .hostNsCountErr ,
424+ }
424425
425- err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi , true )
426+ err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi )
426427
427- require .NoError (t , err )
428- assert .Equal (t , 1 , fakeSubsys .disconnectCalls )
428+ require .NoError (t , err )
429+ assert .Equal (t , test .wantDisconnectCalls , fakeSubsys .disconnectCalls )
430+ })
431+ }
429432}
430433
431434func TestDisconnectNVMeSubsystemIfNeeded_DisconnectErrorPropagates (t * testing.T ) {
@@ -434,7 +437,7 @@ func TestDisconnectNVMeSubsystemIfNeeded_DisconnectErrorPropagates(t *testing.T)
434437 pi := samplePublishInfo (NVMe )
435438 fakeSubsys := & fakeNVMeSubsystem {disconnectErr : errors .New ("disconnect failed" )}
436439
437- err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi , false )
440+ err := core .disconnectNVMeSubsystemIfNeeded (context .Background (), fakeSubsys , pi )
438441
439442 require .Error (t , err )
440443 assert .Contains (t , err .Error (), "disconnect failed" )
0 commit comments