@@ -36,6 +36,18 @@ const (
3636 DevMapperRoot = "/dev/mapper/"
3737 devicesRemovalMaxWaitTime = 5 * time .Second
3838
39+ // multipathDevicePollInterval bounds how often we rescan and re-check sysfs while waiting for a
40+ // multipath dm device during attach.
41+ multipathDevicePollInterval = 300 * time .Millisecond
42+ // multipathDeviceWaitBudgetPercent reserves most of the caller's remaining time budget for waiting on DM readiness.
43+ multipathDeviceWaitBudgetPercent = 80
44+ // multipathDeviceWaitMinTimeout ensures each wait attempt has enough time to make meaningful progress.
45+ multipathDeviceWaitMinTimeout = 5 * time .Second
46+ // multipathDevicePostWaitBuffer keeps some caller budget for post-wait attach steps.
47+ multipathDevicePostWaitBuffer = 5 * time .Second
48+ // multipathDeviceNoDeadlineTimeout bounds wait attempts when callers do not set a context deadline.
49+ multipathDeviceNoDeadlineTimeout = 15 * time .Second
50+
3951 // REDACTED is a copy of what is in utils package.
4052 // we can reference that once we do not have any references into the utils package
4153 REDACTED = "<REDACTED>"
4557 pidRegex = regexp .MustCompile (`^\d+$` )
4658 pidRunningOrIdleRegex = regexp .MustCompile (`pid \d+ (running|idle)` )
4759 tagsWithIndentationRegex = regexp .MustCompile (`(?m)^[\t ]*find_multipaths[\t ]*["|']?(?P<tagName>[\w-_]+)["|']?[\t ]*$` )
60+ fcpRportHostNumberRegex = regexp .MustCompile (`rport-(\d+):` )
4861 beforeFlushDevice = fiji .Register ("fcpBeforeFlushDevice" , "devices" )
4962)
5063
@@ -147,12 +160,15 @@ func (client *Client) AttachVolumeRetry(
147160 var err error
148161 var mpathSize int64
149162
150- if err = client .PreChecks (ctx ); err != nil {
151- return mpathSize , err
163+ attachCtx , cancel := context .WithTimeout (ctx , timeout )
164+ defer cancel ()
165+
166+ if err = client .PreChecks (attachCtx ); err != nil {
167+ return mpathSize , backoff .Permanent (err )
152168 }
153169
154170 checkAttachFCPVolume := func () error {
155- mpathSize , err = client .AttachVolume (ctx , publishInfo )
171+ mpathSize , err = client .AttachVolume (attachCtx , publishInfo )
156172 return err
157173 }
158174
@@ -169,7 +185,7 @@ func (client *Client) AttachVolumeRetry(
169185 attachBackoff .RandomizationFactor = 0.1
170186 attachBackoff .MaxElapsedTime = timeout
171187
172- err = backoff .RetryNotify (checkAttachFCPVolume , attachBackoff , attachNotify )
188+ err = backoff .RetryNotify (checkAttachFCPVolume , backoff . WithContext ( attachBackoff , attachCtx ) , attachNotify )
173189 return mpathSize , err
174190}
175191
@@ -356,10 +372,6 @@ func (client *Client) AttachVolume(
356372 "fstype" : publishInfo .FilesystemType ,
357373 }).Debug ("Attaching FCP SCSI volume." )
358374
359- if err = client .PreChecks (ctx ); err != nil {
360- return mpathSize , err
361- }
362-
363375 // Check if zoning exists for the target
364376 if isZoned , err := client .fcpUtils .CheckZoningExistsWithTarget (ctx , publishInfo .FCTargetWWNN ); ! isZoned {
365377 return mpathSize , err
@@ -614,59 +626,160 @@ func rescanOneLun(ctx context.Context, path string) error {
614626 return nil
615627}
616628
617- // waitForMultipathDeviceForLUN
618- // for the given LUN, this function waits for the associated multipath device to be present
619- // first find the /dev/sd* devices assocaited with the LUN
620- // Wait for the maultipath device dm-* for the /dev/sd* devices.
621- func (client * Client ) waitForMultipathDeviceForLUN (ctx context.Context , hostSessionMap []map [string ]int , lunID int ,
622- fcpNodeName string ,
623- ) error {
624- fields := LogFields {
625- "lunID" : lunID ,
626- "fcpNodeName" : fcpNodeName ,
627- }
628- Logc (ctx ).WithFields (fields ).Debug (">>>> fcp.waitForMultipathDeviceForLUN" )
629- defer Logc (ctx ).WithFields (fields ).Debug ("<<<< fcp.waitForMultipathDeviceForLUN" )
629+ // fcpDeviceAddressesForLUN builds SCSI scan addresses for each host/target path in the FCP host session map.
630+ func (client * Client ) fcpDeviceAddressesForLUN (ctx context.Context , hostSessionMap []map [string ]int , lunID int ) []models.ScsiDeviceAddress {
631+ deviceAddresses := make ([]models.ScsiDeviceAddress , 0 )
630632
631- paths := client .fcpUtils .GetSysfsBlockDirsForLUN (lunID , hostSessionMap )
633+ for _ , hostPortMap := range hostSessionMap {
634+ for host := range hostPortMap {
635+ hostStr := fcpRportHostNumberRegex .FindStringSubmatch (host )
636+ if len (hostStr ) <= 1 {
637+ Logc (ctx ).WithField ("host" , host ).Debug ("Could not parse host number from host session map key." )
638+ continue
639+ }
640+ hostNum , err := strconv .Atoi (hostStr [1 ])
641+ if err != nil {
642+ Logc (ctx ).WithField ("hostNumber" , hostStr [1 ]).Debug ("Could not parse host number" )
643+ continue
644+ }
632645
633- devices , err := client .fcpUtils .GetDevicesForLUN (paths )
634- if err != nil {
635- return err
636- }
646+ targetIDFile := fmt .Sprintf ("/sys/class/fc_host/host%d/device/%s/fc_remote_ports/%s/scsi_target_id" ,
647+ hostNum , host , host )
648+ targetIDRaw , err := os .ReadFile (targetIDFile )
649+ if err != nil {
650+ Logc (ctx ).WithField ("targetIDFile" , targetIDFile ).Debug ("Could not find target ID file while waiting." )
651+ continue
652+ }
653+ targetIDStr := strings .TrimSpace (string (targetIDRaw ))
654+ targetID , err := strconv .Atoi (targetIDStr )
655+ if err != nil {
656+ Logc (ctx ).WithField ("targetID" , targetIDStr ).Debug ("Could not parse target ID" )
657+ continue
658+ }
637659
638- _ , err = client .waitForMultipathDeviceForDevices (ctx , devices )
660+ deviceAddress := models.ScsiDeviceAddress {
661+ Host : strconv .Itoa (hostNum ),
662+ Channel : models .ScanAllSCSIDeviceAddress ,
663+ Target : strconv .Itoa (targetID ),
664+ LUN : strconv .Itoa (lunID ),
665+ }
666+ if ! collection .Contains (deviceAddresses , deviceAddress ) {
667+ deviceAddresses = append (deviceAddresses , deviceAddress )
668+ }
669+ }
670+ }
671+ return deviceAddresses
672+ }
639673
640- return err
674+ func fcpHostSessionMapEmpty (hostSessionMap []map [string ]int ) bool {
675+ if len (hostSessionMap ) == 0 {
676+ return true
677+ }
678+ for _ , hostPortMap := range hostSessionMap {
679+ if len (hostPortMap ) > 0 {
680+ return false
681+ }
682+ }
683+ return true
641684}
642685
643- // waitForMultipathDeviceForDevices accepts a list of sd* device names which are associated with same LUN
644- // and waits until a multipath device is present for at least one of those. It returns the name of the
645- // multipath device, or an empty string if multipathd isn't running or there is only one path.
646- func ( client * Client ) waitForMultipathDeviceForDevices (ctx context. Context , devices [] string ) ( string , error ) {
647- fields := LogFields { "devices" : devices }
686+ // waitUntilMultipathDevicePresent polls until a multipath dm-* appears for the LUN's SCSI path devices.
687+ // It inspects sysfs before scanning: ScanTargetLUN runs only when no block devices exist for the LUN yet.
688+ func ( client * Client ) waitUntilMultipathDevicePresent ( ctx context. Context , hostSessionMap [] map [ string ] int , lunID int ) error {
689+ waitCtx , cancel := multipathWaitContext (ctx )
690+ defer cancel ()
648691
649- Logc ( ctx ). WithFields ( fields ). Debug ( ">>>> fcp.waitForMultipathDeviceForDevices" )
650- defer Logc ( ctx ). WithFields ( fields ). Debug ( "<<<< fcp.waitForMultipathDeviceForDevices" )
692+ tick := time . NewTicker ( multipathDevicePollInterval )
693+ defer tick . Stop ( )
651694
652- multipathDevice := ""
695+ for {
696+ if err := waitCtx .Err (); err != nil {
697+ return fmt .Errorf ("waiting for multipath device for LUN %d: %w" , lunID , err )
698+ }
699+ paths := client .fcpUtils .GetSysfsBlockDirsForLUN (lunID , hostSessionMap )
700+ devices , err := client .fcpUtils .GetDevicesForLUN (paths )
701+ if err != nil {
702+ return err
703+ }
704+ if len (devices ) == 0 {
705+ addrs := client .fcpDeviceAddressesForLUN (waitCtx , hostSessionMap , lunID )
706+ if len (paths ) == 0 && len (addrs ) == 0 {
707+ if fcpHostSessionMapEmpty (hostSessionMap ) {
708+ return fmt .Errorf ("multipath device not found when it is expected for LUN %d" , lunID )
709+ }
710+ } else if len (addrs ) > 0 {
711+ if err := client .deviceClient .ScanTargetLUN (waitCtx , addrs ); err != nil {
712+ Logc (ctx ).WithField ("scanError" , err ).Debug ("Could not scan for new LUN while waiting for multipath." )
713+ }
714+ }
715+ select {
716+ case <- waitCtx .Done ():
717+ return fmt .Errorf ("waiting for multipath device for LUN %d: %w" , lunID , waitCtx .Err ())
718+ case <- tick .C :
719+ }
720+ continue
721+ }
722+ for _ , device := range devices {
723+ if dm := client .deviceClient .FindMultipathDeviceForDevice (waitCtx , device ); dm != "" {
724+ Logc (ctx ).WithFields (LogFields {"multipathDevice" : dm , "lunID" : lunID }).Debug (
725+ "Multipath device found for LUN." )
726+ return nil
727+ }
728+ }
729+ select {
730+ case <- waitCtx .Done ():
731+ return fmt .Errorf ("waiting for multipath device for LUN %d: %w" , lunID , waitCtx .Err ())
732+ case <- tick .C :
733+ }
734+ }
735+ }
653736
654- for _ , device := range devices {
655- multipathDevice = client .deviceClient .FindMultipathDeviceForDevice (ctx , device )
656- if multipathDevice != "" {
657- break
737+ func multipathWaitContext (ctx context.Context ) (context.Context , context.CancelFunc ) {
738+ if deadline , ok := ctx .Deadline (); ok {
739+ remaining := time .Until (deadline )
740+ if remaining <= 0 {
741+ return context .WithTimeout (ctx , 0 )
742+ }
743+ // Spend most (but not all) of the remaining request budget on the DM wait.
744+ // This avoids consuming the entire deadline in the wait loop and preserves
745+ // time for post-wait attach steps (serial/size/device readiness checks).
746+ sliced := remaining * multipathDeviceWaitBudgetPercent / 100
747+ // Enforce a minimum wait window so very short remaining deadlines don't
748+ // degrade into immediate retry churn with no meaningful polling.
749+ if sliced < multipathDeviceWaitMinTimeout {
750+ sliced = multipathDeviceWaitMinTimeout
751+ }
752+ // Always reserve a small post-wait budget for follow-on operations.
753+ maxWait := remaining - multipathDevicePostWaitBuffer
754+ if maxWait <= 0 {
755+ maxWait = remaining
658756 }
757+ // Ceiling wins: if the minimum floor exceeds the allowed max wait, clamp to maxWait.
758+ if sliced > maxWait {
759+ sliced = maxWait
760+ }
761+ return context .WithTimeout (ctx , sliced )
659762 }
660763
661- if multipathDevice == "" {
662- Logc ( ctx ). WithField ( "multipathDevice" , multipathDevice ). Warn ( "Multipath device not found." )
663- return "" , errors . New ( "multipath device not found when it is expected" )
764+ // Defensive fallback for callers that do not supply a deadline.
765+ return context . WithTimeout ( ctx , multipathDeviceNoDeadlineTimeout )
766+ }
664767
665- } else {
666- Logc (ctx ).WithField ("multipathDevice" , multipathDevice ).Debug ("Multipath device found." )
768+ // waitForMultipathDeviceForLUN
769+ // for the given LUN, this function waits for the associated multipath device to be present
770+ // first find the /dev/sd* devices associated with the LUN
771+ // Wait for the multipath device dm-* for the /dev/sd* devices.
772+ func (client * Client ) waitForMultipathDeviceForLUN (ctx context.Context , hostSessionMap []map [string ]int , lunID int ,
773+ fcpNodeName string ,
774+ ) error {
775+ fields := LogFields {
776+ "lunID" : lunID ,
777+ "fcpNodeName" : fcpNodeName ,
667778 }
779+ Logc (ctx ).WithFields (fields ).Debug (">>>> fcp.waitForMultipathDeviceForLUN" )
780+ defer Logc (ctx ).WithFields (fields ).Debug ("<<<< fcp.waitForMultipathDeviceForLUN" )
668781
669- return multipathDevice , nil
782+ return client . waitUntilMultipathDevicePresent ( ctx , hostSessionMap , lunID )
670783}
671784
672785// waitForDeviceScan scans all paths to a specific LUN and waits until all
@@ -683,46 +796,7 @@ func (client *Client) waitForDeviceScan(ctx context.Context, hostSessionMap []ma
683796
684797 Logc (ctx ).WithField ("hostSessionMap" , hostSessionMap ).Debug ("Built FCP host/session map." )
685798
686- deviceAddresses := make ([]models.ScsiDeviceAddress , 0 )
687- var hostNum , targetID int
688- var err error
689-
690- // Construct the host and target lists required for the scan.
691- for _ , hostNumber := range hostSessionMap {
692- for host := range hostNumber {
693- re := regexp .MustCompile (`rport-(\d+):` )
694- hostStr := re .FindStringSubmatch (host )
695- if len (hostStr ) > 1 {
696- if hostNum , err = strconv .Atoi (hostStr [1 ]); err != nil {
697- Logc (ctx ).WithField ("port" , hostNum ).Error ("Could not parse port number" )
698- continue
699- }
700- }
701-
702- // Read the target IDs from the sysfs path.
703- targetIDFile := fmt .Sprintf ("/sys/class/fc_host/host%d/device/%s/fc_remote_ports/%s/scsi_target_id" ,
704- hostNum , host , host )
705- if targetIDRaw , err := os .ReadFile (targetIDFile ); err != nil {
706- Logc (ctx ).WithField ("targetIDFile" , targetIDFile ).Error ("Could not find the target ID file" )
707- } else {
708- targetIDStr := strings .TrimSpace (string (targetIDRaw ))
709- if targetID , err = strconv .Atoi (targetIDStr ); err != nil {
710- Logc (ctx ).WithField ("targetID" , targetIDStr ).Error ("Could not parse target ID" )
711- continue
712- }
713-
714- deviceAddress := models.ScsiDeviceAddress {
715- Host : strconv .Itoa (hostNum ),
716- Channel : models .ScanAllSCSIDeviceAddress ,
717- Target : strconv .Itoa (targetID ),
718- LUN : strconv .Itoa (lunID ),
719- }
720- if ! collection .Contains (deviceAddresses , deviceAddress ) {
721- deviceAddresses = append (deviceAddresses , deviceAddress )
722- }
723- }
724- }
725- }
799+ deviceAddresses := client .fcpDeviceAddressesForLUN (ctx , hostSessionMap , lunID )
726800
727801 if err := client .deviceClient .ScanTargetLUN (ctx , deviceAddresses ); err != nil {
728802 Logc (ctx ).WithField ("scanError" , err ).Error ("Could not scan for new LUN." )
0 commit comments