On Thu, Dec 04, 2025 at 02:33:34PM -0500, Benjamin Marzinski wrote:
> On Wed, Dec 03, 2025 at 08:19:33AM -0800, Brian Bunker wrote:
> > In the Linux kernel when volumes are disconnected, they are left behind
> > to be re-enabled when the connection comes back. There are many cases
> > where this behavior is not desirable.
> >
> > The goal is to have paths appear when volumes are connected. This is
> > handled by target initiated rescans and posting of unit attentions.
> > Secondarily when volumes are disconnected at the target, paths should
> > be removed.
> >
> > This was discussed here in a kernel patch:
> > https://marc.info/?l=linux-scsi&m=164426722617834&w=2
> >
> > It was suggested there that userspace would be more advantageous in
> > handling the device removal.
> >
> > An option was added to multipath-tools to purge devices that are
> > disconnected at the target. A purge thread was implemented to handle
> > the device removal when the path returns sense data (0x5/0x25/0x0) from
> > the path checker. The option, purge_disconnected, is by default off but
> > can be turned on by setting it to true.
> >
> > When enabled multipathd will log:
> > Dec 3 09:10:05 init110-14 multipathd[54768]: 8:128: path removed from
> > map 3624a93708ebb224d4a5e45c400011a43
> > ...
> > Dec 3 09:10:14 init110-14 multipathd[54768]: purge cycle 2 complete: 3
> > purged, 0 failed, 0 pending
> >
> > Signed-off-by: Brian Bunker <[email protected]>
> > Signed-off-by: Krishna Kant <[email protected]>
> > ---
> > libmultipath/checkers.c | 2 +
> > libmultipath/checkers.h | 2 +
> > libmultipath/checkers/tur.c | 10 +
> > libmultipath/config.c | 2 +
> > libmultipath/config.h | 3 +
> > libmultipath/configure.c | 1 +
> > libmultipath/defaults.h | 1 +
> > libmultipath/dict.c | 14 ++
> > libmultipath/discovery.c | 3 +-
> > libmultipath/io_err_stat.c | 1 +
> > libmultipath/libmultipath.version | 1 +
> > libmultipath/print.c | 2 +
> > libmultipath/propsel.c | 16 ++
> > libmultipath/propsel.h | 1 +
> > libmultipath/structs.h | 10 +
> > libmultipath/sysfs.c | 58 ++++-
> > libmultipath/sysfs.h | 2 +
> > multipath/multipath.conf.5.in | 22 ++
> > multipathd/main.c | 402 +++++++++++++++++++++++++++++-
> > 19 files changed, 545 insertions(+), 8 deletions(-)
> >
> > diff --git a/libmultipath/checkers.c b/libmultipath/checkers.c
> > index e2eda58d..bb6ad1ee 100644
> > --- a/libmultipath/checkers.c
> > +++ b/libmultipath/checkers.c
> > @@ -43,6 +43,7 @@ static const char *checker_state_names[PATH_MAX_STATE] = {
> > [PATH_TIMEOUT] = "timeout",
> > [PATH_REMOVED] = "removed",
> > [PATH_DELAYED] = "delayed",
> > + [PATH_DISCONNECTED] = "disconnected",
> > };
> >
> > static LIST_HEAD(checkers);
> > @@ -363,6 +364,7 @@ static const char
> > *generic_msg[CHECKER_GENERIC_MSGTABLE_SIZE] = {
> > [CHECKER_MSGID_DOWN] = " reports path is down",
> > [CHECKER_MSGID_GHOST] = " reports path is ghost",
> > [CHECKER_MSGID_UNSUPPORTED] = " doesn't support this device",
> > + [CHECKER_MSGID_DISCONNECTED] = " no access to this device",
> > };
> >
> > const char *checker_message(const struct checker *c)
> > diff --git a/libmultipath/checkers.h b/libmultipath/checkers.h
> > index da91f499..903c3ebe 100644
> > --- a/libmultipath/checkers.h
> > +++ b/libmultipath/checkers.h
> > @@ -78,6 +78,7 @@ enum path_check_state {
> > PATH_TIMEOUT,
> > PATH_REMOVED,
> > PATH_DELAYED,
> > + PATH_DISCONNECTED,
> > PATH_MAX_STATE
> > };
> >
> > @@ -113,6 +114,7 @@ enum {
> > CHECKER_MSGID_DOWN,
> > CHECKER_MSGID_GHOST,
> > CHECKER_MSGID_UNSUPPORTED,
> > + CHECKER_MSGID_DISCONNECTED,
> > CHECKER_GENERIC_MSGTABLE_SIZE,
> > CHECKER_FIRST_MSGID = 100, /* lowest msgid for checkers */
> > CHECKER_MSGTABLE_SIZE = 100, /* max msg table size for checkers */
> > diff --git a/libmultipath/checkers/tur.c b/libmultipath/checkers/tur.c
> > index 0010acf8..1bb9a992 100644
> > --- a/libmultipath/checkers/tur.c
> > +++ b/libmultipath/checkers/tur.c
> > @@ -199,6 +199,16 @@ retry:
> > return PATH_PENDING;
> > }
> > }
> > + else if (key == 0x5) {
> > + /* Illegal request */
> > + if (asc == 0x25 && ascq == 0x00) {
> > + /*
> > + * LOGICAL UNIT NOT SUPPORTED
> > + */
> > + *msgid = CHECKER_MSGID_DISCONNECTED;
> > + return PATH_DISCONNECTED;
> > + }
> > + }
> > *msgid = CHECKER_MSGID_DOWN;
> > return PATH_DOWN;
> > }
> > diff --git a/libmultipath/config.c b/libmultipath/config.c
> > index 8b424d18..2c66a788 100644
> > --- a/libmultipath/config.c
> > +++ b/libmultipath/config.c
> > @@ -470,6 +470,7 @@ merge_hwe (struct hwentry * dst, struct hwentry * src)
> > merge_num(marginal_path_err_rate_threshold);
> > merge_num(marginal_path_err_recheck_gap_time);
> > merge_num(marginal_path_double_failed_time);
> > + merge_num(purge_disconnected);
> >
> > snprintf(id, sizeof(id), "%s/%s", dst->vendor, dst->product);
> > reconcile_features_with_options(id, &dst->features,
> > @@ -517,6 +518,7 @@ merge_mpe(struct mpentry *dst, struct mpentry *src)
> > merge_num(skip_kpartx);
> > merge_num(max_sectors_kb);
> > merge_num(ghost_delay);
> > + merge_num(purge_disconnected);
> > merge_num(uid);
> > merge_num(gid);
> > merge_num(mode);
> > diff --git a/libmultipath/config.h b/libmultipath/config.h
> > index 5b4ebf8c..618fa572 100644
> > --- a/libmultipath/config.h
> > +++ b/libmultipath/config.h
> > @@ -88,6 +88,7 @@ struct hwentry {
> > int marginal_path_err_rate_threshold;
> > int marginal_path_err_recheck_gap_time;
> > int marginal_path_double_failed_time;
> > + int purge_disconnected;
> > int skip_kpartx;
> > int max_sectors_kb;
> > int ghost_delay;
> > @@ -130,6 +131,7 @@ struct mpentry {
> > int marginal_path_err_rate_threshold;
> > int marginal_path_err_recheck_gap_time;
> > int marginal_path_double_failed_time;
> > + int purge_disconnected;
> > int skip_kpartx;
> > int max_sectors_kb;
> > int ghost_delay;
> > @@ -188,6 +190,7 @@ struct config {
> > int marginal_path_err_rate_threshold;
> > int marginal_path_err_recheck_gap_time;
> > int marginal_path_double_failed_time;
> > + int purge_disconnected;
> > int uxsock_timeout;
> > int strict_timing;
> > int retrigger_tries;
> > diff --git a/libmultipath/configure.c b/libmultipath/configure.c
> > index baa13573..e4431d2f 100644
> > --- a/libmultipath/configure.c
> > +++ b/libmultipath/configure.c
> > @@ -355,6 +355,7 @@ int setup_map(struct multipath *mpp, char **params,
> > struct vectors *vecs)
> > select_max_sectors_kb(conf, mpp);
> > select_ghost_delay(conf, mpp);
> > select_flush_on_last_del(conf, mpp);
> > + select_purge_disconnected(conf, mpp);
> >
> > sysfs_set_scsi_tmo(conf, mpp);
> > marginal_pathgroups = conf->marginal_pathgroups;
> > diff --git a/libmultipath/defaults.h b/libmultipath/defaults.h
> > index 134b690a..d80dd7d4 100644
> > --- a/libmultipath/defaults.h
> > +++ b/libmultipath/defaults.h
> > @@ -58,6 +58,7 @@
> > #define DEFAULT_ALL_TG_PT ALL_TG_PT_OFF
> > #define DEFAULT_RECHECK_WWID RECHECK_WWID_OFF
> > #define DEFAULT_AUTO_RESIZE AUTO_RESIZE_NEVER
> > +#define DEFAULT_PURGE_DISCONNECTED PURGE_DISCONNECTED_OFF
> > /* Enable no foreign libraries by default */
> > #define DEFAULT_ENABLE_FOREIGN "NONE"
> >
> > diff --git a/libmultipath/dict.c b/libmultipath/dict.c
> > index a06a6138..504b28b5 100644
> > --- a/libmultipath/dict.c
> > +++ b/libmultipath/dict.c
> > @@ -941,6 +941,16 @@ declare_hw_snprint(skip_kpartx, print_yes_no_undef)
> > declare_mp_handler(skip_kpartx, set_yes_no_undef)
> > declare_mp_snprint(skip_kpartx, print_yes_no_undef)
> >
> > +declare_def_handler(purge_disconnected, set_yes_no_undef)
> > +declare_def_snprint_defint(purge_disconnected, print_yes_no_undef,
> > + DEFAULT_PURGE_DISCONNECTED)
> > +declare_ovr_handler(purge_disconnected, set_yes_no_undef)
> > +declare_ovr_snprint(purge_disconnected, print_yes_no_undef)
> > +declare_hw_handler(purge_disconnected, set_yes_no_undef)
> > +declare_hw_snprint(purge_disconnected, print_yes_no_undef)
> > +declare_mp_handler(purge_disconnected, set_yes_no_undef)
> > +declare_mp_snprint(purge_disconnected, print_yes_no_undef)
> > +
> > declare_def_range_handler(remove_retries, 0, INT_MAX)
> > declare_def_snprint(remove_retries, print_int)
> >
> > @@ -2227,6 +2237,7 @@ init_keywords(vector keywords)
> > install_keyword("retrigger_delay", &def_retrigger_delay_handler,
> > &snprint_def_retrigger_delay);
> > install_keyword("missing_uev_wait_timeout",
> > &def_uev_wait_timeout_handler, &snprint_def_uev_wait_timeout);
> > install_keyword("skip_kpartx", &def_skip_kpartx_handler,
> > &snprint_def_skip_kpartx);
> > + install_keyword("purge_disconnected", &def_purge_disconnected_handler,
> > &snprint_def_purge_disconnected);
> > install_keyword("disable_changed_wwids",
> > &deprecated_disable_changed_wwids_handler, &snprint_deprecated);
> > install_keyword("remove_retries", &def_remove_retries_handler,
> > &snprint_def_remove_retries);
> > install_keyword("max_sectors_kb", &def_max_sectors_kb_handler,
> > &snprint_def_max_sectors_kb);
> > @@ -2310,6 +2321,7 @@ init_keywords(vector keywords)
> > install_keyword("marginal_path_err_recheck_gap_time",
> > &hw_marginal_path_err_recheck_gap_time_handler,
> > &snprint_hw_marginal_path_err_recheck_gap_time);
> > install_keyword("marginal_path_double_failed_time",
> > &hw_marginal_path_double_failed_time_handler,
> > &snprint_hw_marginal_path_double_failed_time);
> > install_keyword("skip_kpartx", &hw_skip_kpartx_handler,
> > &snprint_hw_skip_kpartx);
> > + install_keyword("purge_disconnected", &hw_purge_disconnected_handler,
> > &snprint_hw_purge_disconnected);
> > install_keyword("max_sectors_kb", &hw_max_sectors_kb_handler,
> > &snprint_hw_max_sectors_kb);
> > install_keyword("ghost_delay", &hw_ghost_delay_handler,
> > &snprint_hw_ghost_delay);
> > install_keyword("all_tg_pt", &hw_all_tg_pt_handler,
> > &snprint_hw_all_tg_pt);
> > @@ -2355,6 +2367,7 @@ init_keywords(vector keywords)
> > install_keyword("marginal_path_double_failed_time",
> > &ovr_marginal_path_double_failed_time_handler,
> > &snprint_ovr_marginal_path_double_failed_time);
> >
> > install_keyword("skip_kpartx", &ovr_skip_kpartx_handler,
> > &snprint_ovr_skip_kpartx);
> > + install_keyword("purge_disconnected", &ovr_purge_disconnected_handler,
> > &snprint_ovr_purge_disconnected);
> > install_keyword("max_sectors_kb", &ovr_max_sectors_kb_handler,
> > &snprint_ovr_max_sectors_kb);
> > install_keyword("ghost_delay", &ovr_ghost_delay_handler,
> > &snprint_ovr_ghost_delay);
> > install_keyword("all_tg_pt", &ovr_all_tg_pt_handler,
> > &snprint_ovr_all_tg_pt);
> > @@ -2400,6 +2413,7 @@ init_keywords(vector keywords)
> > install_keyword("marginal_path_err_recheck_gap_time",
> > &mp_marginal_path_err_recheck_gap_time_handler,
> > &snprint_mp_marginal_path_err_recheck_gap_time);
> > install_keyword("marginal_path_double_failed_time",
> > &mp_marginal_path_double_failed_time_handler,
> > &snprint_mp_marginal_path_double_failed_time);
> > install_keyword("skip_kpartx", &mp_skip_kpartx_handler,
> > &snprint_mp_skip_kpartx);
> > + install_keyword("purge_disconnected", &mp_purge_disconnected_handler,
> > &snprint_mp_purge_disconnected);
> > install_keyword("max_sectors_kb", &mp_max_sectors_kb_handler,
> > &snprint_mp_max_sectors_kb);
> > install_keyword("ghost_delay", &mp_ghost_delay_handler,
> > &snprint_mp_ghost_delay);
> > install_sublevel_end();
> > diff --git a/libmultipath/discovery.c b/libmultipath/discovery.c
> > index 31db8758..46274f3f 100644
> > --- a/libmultipath/discovery.c
> > +++ b/libmultipath/discovery.c
> > @@ -2482,7 +2482,8 @@ int pathinfo(struct path *pp, struct config *conf,
> > int mask)
> > pp->state == PATH_UNCHECKED ||
> > pp->state == PATH_WILD)
> > pp->chkrstate = pp->state = newstate;
> > - if (pp->state == PATH_TIMEOUT)
> > + if (pp->state == PATH_TIMEOUT ||
> > + pp->state == PATH_DISCONNECTED)
> > pp->state = PATH_DOWN;
> > if (pp->state == PATH_UP && !pp->size) {
> > condlog(3, "%s: device size is 0, "
> > diff --git a/libmultipath/io_err_stat.c b/libmultipath/io_err_stat.c
> > index 64054c18..acb087b7 100644
> > --- a/libmultipath/io_err_stat.c
> > +++ b/libmultipath/io_err_stat.c
> > @@ -397,6 +397,7 @@ static void account_async_io_state(struct
> > io_err_stat_path *pp, int rc)
> > switch (rc) {
> > case PATH_DOWN:
> > case PATH_TIMEOUT:
> > + case PATH_DISCONNECTED:
> > pp->io_err_nr++;
> > break;
> > case PATH_UNCHECKED:
> > diff --git a/libmultipath/libmultipath.version
> > b/libmultipath/libmultipath.version
> > index 89ae2a3c..c435847f 100644
> > --- a/libmultipath/libmultipath.version
> > +++ b/libmultipath/libmultipath.version
> > @@ -239,6 +239,7 @@ global:
> > snprint_tgt_wwnn;
> > snprint_tgt_wwpn;
> > sysfs_attr_set_value;
> > + sysfs_attr_set_value_nonblock;
> > sysfs_attr_get_value;
> > sysfs_get_asymmetric_access_state;
> >
> > diff --git a/libmultipath/print.c b/libmultipath/print.c
> > index 0d44a5a9..8b09b174 100644
> > --- a/libmultipath/print.c
> > +++ b/libmultipath/print.c
> > @@ -541,6 +541,8 @@ snprint_chk_state (struct strbuf *buff, const struct
> > path * pp)
> > return append_strbuf_str(buff, "i/o timeout");
> > case PATH_DELAYED:
> > return append_strbuf_str(buff, "delayed");
> > + case PATH_DISCONNECTED:
> > + return append_strbuf_str(buff, "disconnected");
> > default:
> > return append_strbuf_str(buff, "undef");
> > }
> > diff --git a/libmultipath/propsel.c b/libmultipath/propsel.c
> > index 4c0fbcf3..9a17f1ed 100644
> > --- a/libmultipath/propsel.c
> > +++ b/libmultipath/propsel.c
> > @@ -1371,6 +1371,22 @@ out:
> > return 0;
> > }
> >
> > +int select_purge_disconnected (struct config *conf, struct multipath * mp)
> > +{
> > + const char *origin;
> > +
> > + mp_set_mpe(purge_disconnected);
> > + mp_set_ovr(purge_disconnected);
> > + mp_set_hwe(purge_disconnected);
> > + mp_set_conf(purge_disconnected);
> > + mp_set_default(purge_disconnected, DEFAULT_PURGE_DISCONNECTED);
> > +out:
> > + condlog(3, "%s: purge_disconnected = %s %s", mp->alias,
> > + (mp->purge_disconnected == PURGE_DISCONNECTED_ON)? "yes" : "no",
> > + origin);
> > + return 0;
> > +}
> > +
> > int select_max_sectors_kb(struct config *conf, struct multipath * mp)
> > {
> > const char *origin;
> > diff --git a/libmultipath/propsel.h b/libmultipath/propsel.h
> > index 55930050..c6c937c3 100644
> > --- a/libmultipath/propsel.h
> > +++ b/libmultipath/propsel.h
> > @@ -39,6 +39,7 @@ int select_marginal_path_err_rate_threshold(struct config
> > *conf, struct multipat
> > int select_marginal_path_err_recheck_gap_time(struct config *conf, struct
> > multipath *mp);
> > int select_marginal_path_double_failed_time(struct config *conf, struct
> > multipath *mp);
> > int select_ghost_delay(struct config *conf, struct multipath * mp);
> > +int select_purge_disconnected (struct config *conf, struct multipath * mp);
> > void reconcile_features_with_options(const char *id, char **features,
> > int* no_path_retry,
> > int *retain_hwhandler);
> > diff --git a/libmultipath/structs.h b/libmultipath/structs.h
> > index 9247e74f..81ac6be5 100644
> > --- a/libmultipath/structs.h
> > +++ b/libmultipath/structs.h
> > @@ -186,6 +186,12 @@ enum auto_resize_state {
> > AUTO_RESIZE_GROW_SHRINK,
> > };
> >
> > +enum purge_disconnected_states {
> > + PURGE_DISCONNECTED_UNDEF = YNU_UNDEF,
> > + PURGE_DISCONNECTED_OFF = YNU_NO,
> > + PURGE_DISCONNECTED_ON = YNU_YES,
> > +};
> > +
> > #define PROTOCOL_UNSET -1
> >
> > enum scsi_protocol {
> > @@ -382,6 +388,9 @@ struct path {
> > int dmstate;
> > int chkrstate;
> > int oldstate;
> > + bool purge_path;
> > + int purge_retries; /* number of purge attempts for this path */
> > + unsigned int purge_cycle; /* last purge cycle that checked this path */
> > int failcount;
> > int priority;
> > int pgindex;
> > @@ -483,6 +492,7 @@ struct multipath {
> > int ghost_delay;
> > int ghost_delay_tick;
> > int queue_mode;
> > + int purge_disconnected;
> > unsigned int sync_tick;
> > int checker_count;
> > enum prio_update_type prio_update;
> > diff --git a/libmultipath/sysfs.c b/libmultipath/sysfs.c
> > index af27d107..2160d7fd 100644
> > --- a/libmultipath/sysfs.c
> > +++ b/libmultipath/sysfs.c
> > @@ -134,7 +134,7 @@ ssize_t sysfs_attr_set_value(struct udev_device *dev,
> > const char *attr_name,
> > size = write(fd, value, value_len);
> > if (size < 0) {
> > size = -errno;
> > - condlog(3, "%s: write to %s failed: %s", __func__,
> > + condlog(3, "%s: write to %s failed: %s", __func__,
> > devpath, strerror(errno));
> > } else if (size < (ssize_t)value_len)
> > condlog(3, "%s: underflow writing %zu bytes to %s. Wrote %zd
> > bytes",
> > @@ -144,6 +144,62 @@ ssize_t sysfs_attr_set_value(struct udev_device *dev,
> > const char *attr_name,
> > return size;
> > }
>
> Unfortunately. This doesn't work at all. O_NONBLOCK doesn't mean
> anything for sysfs writes. The only way to make the write non-blocking
> would be to use libaio or io_uring. But we're already doing the write in
> a separate thread without holding any locks, so we're fine with it
> blocking.
>
> >
> > +/*
> > + * Non-blocking version of sysfs_attr_set_value for use with potentially
> > + * blocking sysfs attributes (like SCSI "delete").
> > + *
> > + * Returns:
> > + * > 0: number of bytes written (success)
> > + * -EAGAIN: write would block, caller should retry or wait for completion
> > + * < 0: other error (negative errno)
> > + */
> > +ssize_t sysfs_attr_set_value_nonblock(struct udev_device *dev, const char
> > *attr_name,
> > + const char * value, size_t value_len)
> > +{
> > + const char *syspath;
> > + char devpath[PATH_MAX];
> > + int fd = -1;
> > + ssize_t size = -1;
> > +
> > + if (!dev || !attr_name || !value || !value_len) {
> > + condlog(1, "%s: invalid parameters", __func__);
> > + return -EINVAL;
> > + }
> > +
> > + syspath = udev_device_get_syspath(dev);
> > + if (!syspath) {
> > + condlog(3, "%s: invalid udevice", __func__);
> > + return -EINVAL;
> > + }
> > + if (safe_sprintf(devpath, "%s/%s", syspath, attr_name)) {
> > + condlog(3, "%s: devpath overflow", __func__);
> > + return -EOVERFLOW;
> > + }
> > +
> > + condlog(4, "open '%s' (non-blocking)", devpath);
> > + /* Open with O_NONBLOCK to avoid blocking in open() or write() */
> > + fd = open(devpath, O_WRONLY | O_NONBLOCK);
> > + if (fd < 0) {
> > + condlog(3, "%s: attribute '%s' cannot be opened: %s",
> > + __func__, devpath, strerror(errno));
> > + return -errno;
> > + }
> > + pthread_cleanup_push(cleanup_fd_ptr, &fd);
> > +
> > + size = write(fd, value, value_len);
> > + if (size < 0) {
> > + size = -errno;
> > + if (errno != EAGAIN)
> > + condlog(3, "%s: write to %s failed: %s", __func__,
> > + devpath, strerror(errno));
> > + } else if (size < (ssize_t)value_len)
> > + condlog(3, "%s: underflow writing %zu bytes to %s. Wrote %zd
> > bytes",
> > + __func__, value_len, devpath, size);
> > +
> > + pthread_cleanup_pop(1);
> > + return size;
> > +}
> > +
> > int
> > sysfs_get_size (struct path *pp, unsigned long long * size)
> > {
> > diff --git a/libmultipath/sysfs.h b/libmultipath/sysfs.h
> > index 45f24c37..da2deaa3 100644
> > --- a/libmultipath/sysfs.h
> > +++ b/libmultipath/sysfs.h
> > @@ -10,6 +10,8 @@
> > int devt2devname (char *, int, const char *);
> > ssize_t sysfs_attr_set_value(struct udev_device *dev, const char
> > *attr_name,
> > const char * value, size_t value_len);
> > +ssize_t sysfs_attr_set_value_nonblock(struct udev_device *dev, const char
> > *attr_name,
> > + const char * value, size_t value_len);
> > ssize_t sysfs_attr_get_value(struct udev_device *dev, const char
> > *attr_name,
> > char * value, size_t value_len);
> > ssize_t sysfs_bin_attr_get_value(struct udev_device *dev, const char
> > *attr_name,
> > diff --git a/multipath/multipath.conf.5.in b/multipath/multipath.conf.5.in
> > index 3c9ae097..84cd1a0a 100644
> > --- a/multipath/multipath.conf.5.in
> > +++ b/multipath/multipath.conf.5.in
> > @@ -1306,6 +1306,22 @@ The default is: \fBno\fR
> > .
> > .
> > .TP
> > +.B purge_disconnected
> > +If set to
> > +.I yes
> > +, multipathd will automatically remove devices that are in a disconnected
> > state.
> > +A path is considered disconnected when the TUR (Test Unit Ready) path
> > checker
> > +receives the SCSI sense code "LOGICAL UNIT NOT SUPPORTED" (sense key 0x5,
> > +ASC/ASCQ 0x25/0x00). This typically indicates that the LUN has been
> > unmapped
> > +or is no longer presented by the storage array. This option helps clean up
> > +stale device entries that would otherwise remain in the system.
> > +.RS
> > +.TP
> > +The default is: \fBno\fR
> > +.RE
> > +.
> > +.
> > +.TP
> > .B disable_changed_wwids
> > (Deprecated) This option is not supported anymore, and will be ignored.
> > .RE
> > @@ -1602,6 +1618,8 @@ section:
> > .TP
> > .B skip_kpartx
> > .TP
> > +.B purge_disconnected
> > +.TP
> > .B max_sectors_kb
> > .TP
> > .B ghost_delay
> > @@ -1797,6 +1815,8 @@ section:
> > .TP
> > .B skip_kpartx
> > .TP
> > +.B purge_disconnected
> > +.TP
> > .B max_sectors_kb
> > .TP
> > .B ghost_delay
> > @@ -1881,6 +1901,8 @@ the values are taken from the \fIdevices\fR or
> > \fIdefaults\fR sections:
> > .TP
> > .B skip_kpartx
> > .TP
> > +.B purge_disconnected
> > +.TP
> > .B max_sectors_kb
> > .TP
> > .B ghost_delay
> > diff --git a/multipathd/main.c b/multipathd/main.c
> > index d11a8576..b105cd56 100644
> > --- a/multipathd/main.c
> > +++ b/multipathd/main.c
> > @@ -137,11 +137,14 @@ static enum force_reload_types reconfigure_pending =
> > FORCE_RELOAD_NONE;
> > pid_t daemon_pid;
> > static pthread_mutex_t config_lock = PTHREAD_MUTEX_INITIALIZER;
> > static pthread_cond_t config_cond;
> > -static pthread_t check_thr, uevent_thr, uxlsnr_thr, uevq_thr, dmevent_thr,
> > - fpin_thr, fpin_consumer_thr;
> > -static bool check_thr_started, uevent_thr_started, uxlsnr_thr_started,
> > - uevq_thr_started, dmevent_thr_started, fpin_thr_started,
> > - fpin_consumer_thr_started;
> > +static pthread_mutex_t purge_mutex = PTHREAD_MUTEX_INITIALIZER;
> > +static pthread_cond_t purge_cond = PTHREAD_COND_INITIALIZER;
> > +int devices_to_purge = 0;
> > +static pthread_t check_thr, purge_thr, uevent_thr, uxlsnr_thr, uevq_thr,
> > + dmevent_thr, fpin_thr, fpin_consumer_thr;
> > +static bool check_thr_started, purge_thr_started, uevent_thr_started,
> > + uxlsnr_thr_started, uevq_thr_started, dmevent_thr_started,
> > + fpin_thr_started, fpin_consumer_thr_started;
> > static int pid_fd = -1;
> >
> > static inline enum daemon_status get_running_state(bool *pending_reconfig)
> > @@ -1089,6 +1092,109 @@ check_path_wwid_change(struct path *pp)
> > return false;
> > }
>
> This is problematic. Without holding the vecs lock, we cannot trust the
> path to continue to exist. It could go away at any time, and we would be
> pointing at freed memory.
>
> > +/*
> > + * Information needed to delete a path via sysfs, copied while holding the
> > lock
> > + * so that the actual sysfs write can be done without holding vecs->lock.
> > + */
> > +struct purge_path_info {
> > + struct udev_device *udev; /* Reference to udev device (refcounted) */
> > + char dev_t[BLK_DEV_SIZE]; /* For logging */
> > + char alias[WWID_SIZE]; /* mpp alias for logging */
> > + struct path *pp; /* Path pointer for updating purge_path flag
> > */
> > +};
> > +
> > +/*
> > + * Attempt to delete a path by writing to the SCSI device's sysfs delete
> > attribute.
> > + * This triggers kernel-level device removal. The actual cleanup of the
> > path structure
> > + * from pathvec happens later when a uevent arrives (handled by
> > uev_remove_path).
> > + *
> > + * This function does NOT require vecs->lock to be held, as it operates on
> > copied data.
> > + * Uses non-blocking I/O to avoid blocking in scsi_remove_device().
> > + *
> > + * Returns:
> > + * true - sysfs write succeeded or device already being deleted
> > + * false - sysfs write would block (EAGAIN), caller should retry
> > + *
> > + * Note: Errors other than EAGAIN are treated as success, because they
> > typically
> > + * indicate the device is already in SDEV_DEL or SDEV_CANCEL state, or the
> > sysfs
> > + * file doesn't exist (device already removed).
> > + */
>
> Without the vecs lock being held there nothing to make sure that the
> path still exists. This means that it could get removed, at which point
> the devname could get reused. Having a udev reference will not prevent
> that. So, for instance sdc may now be a new device. Whether or not you
> would delete this new device depends on whether it's sysfs path remains
> the same as for the old device. The only real guarantee that the device
> won't get reused is if someone has it open.
>
> > +static bool
> > +delete_path_sysfs(struct purge_path_info *info)
> > +{
> > + struct udev_device *ud;
> > + ssize_t ret;
> > +
> > + if (!info->udev)
> > + return true; /* No device, treat as success */
> > +
> > + ud = udev_device_get_parent_with_subsystem_devtype(info->udev,
> > + "scsi", "scsi_device");
> > +
> > + if (!ud) {
> > + /* No SCSI parent device, likely already removed */
> > + condlog(4, "%s: no SCSI parent device found, likely already
> > removed",
> > + info->dev_t);
> > + return true;
> > + }
> > +
> > + ret = sysfs_attr_set_value_nonblock(ud, "delete", "1", strlen("1"));
> > +
> > + if (ret == -EAGAIN) {
> > + /* Write would block, caller should retry */
> > + condlog(4, "%s: delete would block, will retry", info->dev_t);
> > + return false;
> > + }
> > +
> > + if (ret < 0) {
> > + /*
> > + * Other errors (ENOENT, EINVAL, etc.) typically mean the device
> > + * is already being deleted or is in SDEV_DEL/SDEV_CANCEL state.
> > + * Treat as success since the end result is what we want.
> > + */
> > + condlog(3, "%s: sysfs delete returned %zd (%s), treating as
> > success",
> > + info->dev_t, ret, strerror((int)-ret));
> > + return true;
> > + }
> > +
> > + /* Success */
> > + condlog(2, "%s: initiated removal from %s", info->dev_t, info->alias);
> > + return true;
> > +}
> > +
> > +/*
> > + * Prepare purge info for a path while holding vecs->lock.
> > + * Takes a reference on the udev device so it remains valid after
> > unlocking.
> > + * Returns true if info was successfully prepared, false otherwise.
> > + */
> > +static bool
> > +prepare_purge_path_info(struct path *pp, struct purge_path_info *info)
> > +{
> > + if (!pp->udev || !pp->mpp)
> > + return false;
> > +
> > + info->udev = udev_device_ref(pp->udev);
> > + if (!info->udev)
> > + return false;
> > +
> > + strlcpy(info->dev_t, pp->dev_t, sizeof(info->dev_t));
> > + strlcpy(info->alias, pp->mpp->alias, sizeof(info->alias));
> > + info->pp = pp;
> > + return true;
> > +}
>
> This has to be implemented as function that can get run by a cleanup
> handler. All multipathd threads must cleanup any state (references,
> open fd, locks) if they are cancelled.
>
> > +/*
> > + * Clean up purge info after use.
> > + */
> > +static void
> > +cleanup_purge_path_info(struct purge_path_info *info)
> > +{
> > + if (info->udev) {
> > + udev_device_unref(info->udev);
> > + info->udev = NULL;
> > + }
> > +}
> > +
> > /*
> > * uev_add_path can call uev_update_path, and uev_update_path can call
> > * uev_add_path
> > @@ -2031,6 +2137,10 @@ reinstate_path (struct path * pp)
> > condlog(0, "%s: reinstate failed", pp->dev_t);
> > else {
> > condlog(2, "%s: reinstated", pp->dev_t);
> > + if (pp->purge_path) {
>
> On the off chance that a path gets disconnected, not successfully
> purged, and then reconnected, we should probably clear pp->purge_cycle
> here as well. There is an admittedly very rare chance that the path
> could later be again disconnected after purgeloop's current_cycle rolled
> over and got back to the old value of pp->purge_cycle. This would keep
> it from getting handled, since it would appear to have already been
> handled in the current cycle.
>
> > + pp->purge_path = false;
> > + pp->purge_retries = 0;
> > + }
> > update_queue_mode_add_path(pp->mpp);
> > }
> > }
> > @@ -2474,6 +2584,20 @@ get_new_state(struct path *pp)
> > if (newstate == PATH_REMOVED)
> > newstate = PATH_DOWN;
> >
> > + /*
> > + * For PATH_DISOCONNECTED mark the path as OK to purge if that is
> > + * enabled. Whether or not purge is enabled mark the path as down.
> > + */
> > + if (newstate == PATH_DISCONNECTED) {
> > + if (pp->mpp->purge_disconnected == PURGE_DISCONNECTED_ON &&
> > + !pp->purge_path) {
> > + condlog(2, "%s: mark (%s) path for purge",
> > + pp->dev, checker_state_name(newstate));
> > + pp->purge_path = true;
> > + }
> > + newstate = PATH_DOWN;
> > + }
> > +
> > if (newstate == PATH_WILD || newstate == PATH_UNCHECKED) {
> > condlog(2, "%s: unusable path (%s) - checker failed",
> > pp->dev, checker_state_name(newstate));
> > @@ -3022,6 +3146,25 @@ update_paths(struct vectors *vecs, int *num_paths_p,
> > time_t start_secs)
> > return CHECKER_FINISHED;
> > }
> >
> > +/*
> > + * Check if there are newly marked paths to purge.
> > + * Only returns true for paths with purge_path=true and purge_retries=0,
> > + * which indicates they were just marked for purging and haven't been
> > + * attempted yet. This prevents signaling the purge thread unnecessarily
> > + * for paths that are already being retried.
> > + */
> > +static bool
> > +has_paths_to_purge(struct vectors *vecs)
> > +{
> > + int i;
> > + struct path *pp;
> > +
> > + vector_foreach_slot(vecs->pathvec, pp, i)
> > + if (pp->purge_path && pp->purge_retries == 0)
> > + return true;
> > + return false;
> > +}
> > +
> > static void enable_pathgroups(struct multipath *mpp)
> > {
> > struct pathgroup *pgp;
> > @@ -3125,6 +3268,7 @@ checkerloop (void *ap)
> > int num_paths = 0, strict_timing;
> > unsigned int ticks = 0;
> > enum checker_state checker_state = CHECKER_STARTING;
> > + bool devs_to_purge = false;
> >
> > if (set_config_state(DAEMON_RUNNING) != DAEMON_RUNNING)
> > /* daemon shutdown */
> > @@ -3168,8 +3312,10 @@ checkerloop (void *ap)
> > if (checker_state == CHECKER_UPDATING_PATHS)
> > checker_state = update_paths(vecs, &num_paths,
> > start_time.tv_sec);
> > - if (checker_state == CHECKER_FINISHED)
> > + if (checker_state == CHECKER_FINISHED) {
> > + devs_to_purge = has_paths_to_purge(vecs);
> > checker_finished(vecs, ticks);
> > + }
> > lock_cleanup_pop(vecs->lock);
> > }
> >
> > @@ -3192,6 +3338,13 @@ checkerloop (void *ap)
> > (long)diff_time.tv_sec);
> > }
> >
> > + if (devs_to_purge) {
> > + pthread_mutex_lock(&purge_mutex);
> > + devices_to_purge = 1;
> > + pthread_cond_signal(&purge_cond);
> > + pthread_mutex_unlock(&purge_mutex);
> > + }
> > +
> > if (foreign_tick == 0) {
> > conf = get_multipath_config();
> > foreign_tick = conf->max_checkint;
> > @@ -3229,6 +3382,234 @@ checkerloop (void *ap)
> > return NULL;
> > }
> >
> > +/*
> > + * Purge thread: removes disconnected paths by writing to sysfs.
> > + *
> > + * This thread is signaled by the checker thread when paths marked with
> > + * purge_path=true are detected. It attempts to delete these paths by
> > + * writing "1" to their SCSI device's sysfs "delete" attribute.
> > + *
> > + * The actual cleanup of path structures from pathvec happens
> > asynchronously
> > + * when the kernel sends a remove uevent (handled by uev_remove_path).
> > + * If the uevent doesn't arrive or the sysfs write fails, paths will be
> > + * retried on subsequent purge cycles with a timeout mechanism.
> > + *
> > + * To avoid holding vecs->lock for extended periods and starving other
> > threads,
> > + * this function processes one path at a time: lock -> handle one path ->
> > unlock.
> > + * Paths track which cycle they were checked in to avoid reprocessing.
> > + */
> > +#define PURGE_TIMEOUT_CYCLES 10 /* ~50 seconds at 5 second check
> > intervals */
> > +
> > +static void
> > +reset_purge_cycle_stats(unsigned int *purged, unsigned int *failed,
> > + unsigned int *pending, bool *found)
> > +{
> > + *purged = 0;
> > + *failed = 0;
> > + *pending = 0;
> > + *found = false;
> > +}
>
> How about a simplifying the purge thread slightly. When the checker gets
> a path it is supposed to purge, it sets purge_path, just like now. I
> don't think we need to track purge_retries at all. When the purge loop
> is triggered, it locks the vecs lock, and finds a single path to purge,
> like now. The only info it needs to save is the udev device (with a new
> refcount taken like now) and a dup of pp->fd (You need this to guarantee
> that the devnode won't get reused if the device is deleted and a new one
> is re-added). You could also save the name, but
> udev_device_get_devnode() will get it for you. Like I mentioned above,
> you need to have a pthread cleanup handler that gets run to clean these
> up if they are set and the thread is cancelled, just like how the vecs
> lock is handled. Then purgeloop() clears pp->purge_path, drops the vecs
> lock, does the sysfs delete, and cleans up the purge_info by calling
> pthread_cleanup_pop(). Finally it loops back to reacquiring the vecs
> lock and finding the next path to delete. I don't really see a value in
> all the stats tracking. If it goes through all the paths, and doesn't
> find one in need of purging, it waits, just like now.
>
> I'm not even sure we need to keep the cycle checking code. If we really
> think the sysfs call can fail, and leave the path around, then it may be
> necessary. In that case, if the purge thread took a long time (longer
> than 5 seconds) the checker could set pp->pruge_path again before the
> purge thread got to processing the later paths in need of purging. We
> could get stuck processing the same path over and over. But this only
> really happens if purging takes a long time and then fails.
>
> I also don't think the sleeping code is necessary. If there are paths to
> purge, they are either newly added, or the purge thread has taken long
> that the checker has run again and re-added them. Either way, it seems
> like we should handle them right away.
>
> Martin's point (that this could be done by an entirely separate program
> that get's called with a scsi device, opens it (to make sure it doesn't
> get removed), verifys that it is disconnected, and then removes it via
> sysfs) is reasonable. But if we want to solve this in multipathd,
> limiting the vecs lock holding to just when we set up the
Oops. I'm not sure how the rest of this got deleted. It said something
like:
...
limiting the vecs lock holding to just when we set up the purge_info
should be fine.
-Ben
>
> > +static void *
> > +purgeloop (void *ap)
> > +{
> > + struct vectors *vecs;
> > + unsigned int i;
> > + struct path *pp;
> > + unsigned int current_cycle = 1;
> > + unsigned int devices_purged_total = 0;
> > + unsigned int devices_failed_total = 0;
> > + unsigned int devices_pending_total = 0;
> > + bool found_path_this_cycle = false;
> > + bool cycle_complete_logged = false;
> > + vecs = (struct vectors *)ap;
> > +
> > + pthread_cleanup_push(rcu_unregister, NULL);
> > + rcu_register_thread();
> > + mlockall(MCL_CURRENT | MCL_FUTURE);
> > +
> > + while (1) {
> > + bool path_handled = false;
> > + struct purge_path_info purge_info = { .udev = NULL };
> > + bool do_purge = false;
> > + char dev_name[FILE_NAME_SIZE];
> > +
> > + /*
> > + * Lock and search for one path that needs processing.
> > + * Copy the necessary info while holding the lock, then
> > + * release the lock before doing any blocking sysfs operations.
> > + */
>
> we need to pthread_cleanup_push a handle that cleans up purge_info (if
> it's set) here.
>
> > + pthread_cleanup_push(cleanup_lock, &vecs->lock);
> > + lock(&vecs->lock);
> > + pthread_testcancel();
> > +
> > + vector_foreach_slot (vecs->pathvec, pp, i) {
> > + if (!pp->purge_path || pp->purge_cycle == current_cycle)
> > + continue;
> > +
> > + pp->purge_cycle = current_cycle;
> > + path_handled = true;
> > +
>
> I think we can remove all this retries code.
>
> > + /* Increment timeout counter for this purge attempt */
> > + pp->purge_retries++;
> > +
> > + if (pp->purge_retries >= PURGE_TIMEOUT_CYCLES) {
> > + /*
> > + * Timeout: path couldn't be deleted after
> > multiple attempts.
> > + * This likely means the device still exists in
> > the kernel
> > + * (perhaps it came back online, or the delete
> > is stuck).
> > + *
> > + * We should NOT force-remove it from
> > multipathd's state,
> > + * as that would create a mismatch between
> > multipathd's view
> > + * and the kernel's view of existing devices.
> > + *
> > + * Instead, clear the purge flag and stop
> > trying. The path
> > + * will remain in multipathd. If it's truly
> > disconnected,
> > + * it will be marked for purge again on the
> > next check cycle.
> > + */
> > + condlog(1, "%s: purge timeout after %d
> > attempts, giving up. "
> > + "Device may still exist in kernel.",
> > + pp->dev, pp->purge_retries);
> > + pp->purge_path = false;
> > + pp->purge_retries = 0;
> > + devices_failed_total++;
> > + } else {
> > + /*
> > + * Prepare info for sysfs deletion.
> > + * Copy all needed data while holding the lock.
> > + */
> > + if (prepare_purge_path_info(pp, &purge_info)) {
> > + do_purge = true;
> > + strlcpy(dev_name, pp->dev,
> > sizeof(dev_name));
> > + } else {
> > + /* Can't prepare info, will retry next
> > cycle */
> > + devices_pending_total++;
> > + condlog(4, "%s: failed to prepare purge
> > info, will retry",
> > + pp->dev);
> > + }
> > + }
> > + break;
> > + }
> > +
> > + lock_cleanup_pop(vecs->lock);
> > +
> > + /*
> > + * Now we've released the lock. Do the non-blocking sysfs
> > operation
> > + * without holding vecs->lock.
> > + */
> > + if (do_purge) {
> > + bool success = delete_path_sysfs(&purge_info);
>
> I don't really see the benefit of all this work done with the lock
> reacquired.
>
> > + /*
> > + * Re-acquire lock to update path state.
> > + * The path might have been removed while we didn't
> > hold the lock,
> > + * so we need to verify it still exists.
> > + */
> > + pthread_cleanup_push(cleanup_lock, &vecs->lock);
> > + lock(&vecs->lock);
> > + pthread_testcancel();
> > +
> > + /* Verify the path still exists in pathvec */
> > + if (find_slot(vecs->pathvec, purge_info.pp) != -1) {
> > + if (success) {
> > + /* Success: sysfs write succeeded,
> > uevent expected */
> > + devices_purged_total++;
> > + purge_info.pp->purge_path = false;
> > + purge_info.pp->purge_retries = 0;
> > + condlog(3, "%s: path purge successful",
> > dev_name);
> > + } else {
> > + /* Will retry on next cycle */
> > + devices_pending_total++;
> > + condlog(4, "%s: path purge failed, will
> > retry (attempt %d/%d)",
> > + dev_name,
> > purge_info.pp->purge_retries,
> > + PURGE_TIMEOUT_CYCLES);
> > + }
> > + } else {
> > + /*
> > + * Path was removed while we were doing sysfs
> > write.
> > + * This is actually success - the path is gone,
> > which is what we wanted.
> > + */
> > + if (success) {
> > + devices_purged_total++;
> > + condlog(3, "%s: path removed during
> > purge operation (counted as success)",
> > + dev_name);
> > + } else {
> > + /* Path removed but our sysfs write
> > failed - don't count it */
> > + condlog(4, "%s: path removed during
> > failed purge operation", dev_name);
> > + }
> > + }
> > +
> > + lock_cleanup_pop(vecs->lock);
> > + cleanup_purge_path_info(&purge_info);
> > + }
>
> We clean up the purge_info() here with pthread_cleanup_pop()
>
>
> Again, I don't think we need all this logging and sleeping code.
> If we handled a path we just loop. If not we increment current_cycle
> (assuming we think it is necessary) and wait to get woken back up.
> > + /*
> > + * If we didn't find any path to process, we've completed a
> > cycle.
> > + * Check if we should wait for more work or start a new cycle.
> > + */
> > + if (path_handled)
> > + found_path_this_cycle = true;
> > + else {
> > + if (found_path_this_cycle && !cycle_complete_logged) {
> > + /* Completed a cycle, log statistics */
> > + condlog(2, "purge cycle %u complete: %u purged,
> > %u failed, %u pending",
> > + current_cycle, devices_purged_total,
> > + devices_failed_total,
> > devices_pending_total);
> > + cycle_complete_logged = true;
> > +
> > + /* Check if we need to retry pending paths */
> > + if (devices_pending_total > 0) {
> > + struct timespec wait_time;
> > + struct config *conf;
> > +
> > + conf = get_multipath_config();
> > + wait_time.tv_sec = conf->checkint;
> > + wait_time.tv_nsec = 0;
> > + put_multipath_config(conf);
> > +
> > + condlog(3, "sleeping %ld seconds before
> > next purge cycle",
> > + (long)wait_time.tv_sec);
> > + nanosleep(&wait_time, NULL);
> > +
> > + /* Start a new cycle for pending paths
> > */
> > + current_cycle++;
> > +
> > reset_purge_cycle_stats(&devices_purged_total,
> > +
> > &devices_failed_total,
> > +
> > &devices_pending_total,
> > +
> > &found_path_this_cycle);
> > + cycle_complete_logged = false;
> > + condlog(3, "starting purge cycle %u",
> > current_cycle);
> > + continue;
> > + }
> > +
> > + /* No pending paths, prepare for next cycle */
>
> Just to make sure that paths don't get skipped, we should make sure that
> if current_cycle rolls over to 0, it's incremented again. That way,
> paths with pp->purge_cycle = 0 will always get checked.
>
> -Ben
>
> > + current_cycle++;
> > + reset_purge_cycle_stats(&devices_purged_total,
> > + &devices_failed_total,
> > + &devices_pending_total,
> > + &found_path_this_cycle);
> > + cycle_complete_logged = false;
> > + }
> > +
> > + /* No paths to process, wait for signal */
> > + if (cycle_complete_logged) {
> > + pthread_mutex_lock(&purge_mutex);
> > + while (!devices_to_purge) {
> > + condlog(3, "waiting on devices to
> > purge");
> > + pthread_cond_wait(&purge_cond,
> > &purge_mutex);
> > + }
> > + devices_to_purge = 0;
> > + pthread_mutex_unlock(&purge_mutex);
> > +
> > + condlog(3, "starting purge cycle %u",
> > current_cycle);
> > + }
> > + }
> > + }
> > +
> > + pthread_cleanup_pop(1);
> > + return NULL;
> > +}
> > +
> > static int
> > configure (struct vectors * vecs, enum force_reload_types reload_type)
> > {
> > @@ -3669,6 +4050,8 @@ static void cleanup_threads(void)
> >
> > if (check_thr_started)
> > pthread_cancel(check_thr);
> > + if (purge_thr_started)
> > + pthread_cancel(purge_thr);
> > if (uevent_thr_started)
> > pthread_cancel(uevent_thr);
> > if (uxlsnr_thr_started)
> > @@ -3685,6 +4068,8 @@ static void cleanup_threads(void)
> >
> > if (check_thr_started)
> > pthread_join(check_thr, NULL);
> > + if (purge_thr_started)
> > + pthread_join(purge_thr, NULL);
> > if (uevent_thr_started)
> > pthread_join(uevent_thr, NULL);
> > if (uxlsnr_thr_started)
> > @@ -3937,6 +4322,11 @@ child (__attribute__((unused)) void *param)
> > goto failed;
> > } else
> > check_thr_started = true;
> > + if ((rc = pthread_create(&purge_thr, &misc_attr, purgeloop, vecs))) {
> > + condlog(0,"failed to create purge loop thread: %d", rc);
> > + goto failed;
> > + } else
> > + purge_thr_started = true;
> > if ((rc = pthread_create(&uevq_thr, &misc_attr, uevqloop, vecs))) {
> > condlog(0, "failed to create uevent dispatcher: %d", rc);
> > goto failed;
> > --
> > 2.51.2