Skip to content

apc_modbus: three fixes for the #3414 read-retry loop - #3571

Open
d01 wants to merge 5 commits into
networkupstools:masterfrom
d01:apc-modbus-retry-loop-fixes
Open

apc_modbus: three fixes for the #3414 read-retry loop#3571
d01 wants to merge 5 commits into
networkupstools:masterfrom
d01:apc-modbus-retry-loop-fixes

Conversation

@d01

@d01 d01 commented Aug 14, 2026

Copy link
Copy Markdown

Three defects in the register-read retry loop added by #3414, found while
getting apc_modbus running reliably on an APC Smart-UPS X1500 (051d:0003,
FW "UPS 16.0") over USB. Each is device-independent; the X1500 is just the
hardware that makes all three visible at once. Compile-tested on master and
running in production (backported) on 2.8.4.

1. The loop ignores exit_flag

A driver asked to stop keeps working through its remaining retries — up to
retries × response_timeout, which with non-default settings comfortably
exceeds the 5 s upsdrvctl allows for SIGTERM before escalating to SIGKILL.
On the X1500 a SIGKILL mid-exchange leaves the device unable to serve Modbus
until its USB cable is physically reseated, so every clean shutdown wedged
the UPS. Journal signature before the fix:

Stopping /run/nut/apc_modbus-smx.pid failed, retrying harder: Success
Main process exited, code=killed, status=9/KILL

2. The loop retries only ETIMEDOUT

It gives up on EMBBADSLAVE/EMBBADCRC/EMBBADDATA — the one case where a
retry is most obviously right, because a reply arrived that belongs to an
earlier request, and reading it has already consumed it. On packetised
transports (rtu_usb) a deferred reply stays queued on the endpoint rather
than ageing off the wire, so this is routine: the first driver start after a
host reboot collects a reply queued before the reboot (the self-powered UPS
keeps its state), fails, and systemd restart-loops the driver, degrading the
device further with every failed start.

3. Every attempt is logged at LOG_ERR

The pre-#3414 error log was left inside the new loop, so attempts the next
retry recovers are still reported as errors — the exact case the loop was
added for. Measured here in a degraded state: ~310 LOG_ERR/min while the
connection was never closed once and every read succeeded; ~890k lines/day,
42% of the journal (doubled by upsdrvctl forwarding driver stderr alongside
the syslog write). Attempts now log at upsdebugx(1); the error logs once,
after the loop, with the attempt count.

Context

The wider investigation (the X1500's post-reset behaviour, its reply
queueing, and the transport-level fixes in networkupstools/libmodbus#12) is
written up separately; these three fixes stand on their own and are the
device-independent part. Related: #2609, #3414.

Note on authorship

Developed with assistance from Claude (Anthropic); commits carry a
Co-Authored-By trailer to that effect. All findings were verified on real
hardware.

d01 and others added 3 commits August 14, 2026 07:58
The read retry loop added in networkupstools#3414 does not check exit_flag, so a driver
asked to stop keeps working through its remaining retries: up to
retries * response_timeout, which with non-default settings comfortably
exceeds the 5 seconds upsdrvctl allows for SIGTERM before it escalates to
SIGKILL (drivers/upsdrvctl.c, the "retrying harder" path).

On some hardware that escalation is not merely untidy: an APC Smart-UPS
X1500 (051d:0003, FW "UPS 16.0") killed mid-exchange stops serving Modbus
altogether -- it accepts writes and answers nothing -- and only physically
reseating the USB cable recovers it. Every clean shutdown then wedges the
UPS.

Break out of the loop as soon as exit_flag is set. Verified on that
hardware: shutdowns now log "Signal 15: exiting" and deactivate cleanly
where they previously hit "Stopping ... failed, retrying harder" followed
by status=9/KILL.

Signed-off-by: d01 <d01@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retry loop from networkupstools#3414 retries only on ETIMEDOUT and gives up on every
other error. That leaves out the one case where a retry is most obviously
correct: a reply arrived, but it is not the reply to this request.
libmodbus reports those as EMBBADSLAVE, EMBBADCRC or EMBBADDATA.

On a packetised transport such as rtu_usb this is common, because a reply
the device deferred past our timeout stays queued on the endpoint rather
than ageing off the wire as it would on an idle serial line. The next read
then collects the previous request's answer. Reading it has consumed it,
so the very next attempt can succeed -- but the loop breaks out instead of
making it.

On an APC Smart-UPS X1500 this is exactly what happens on the first driver
start after a host reboot (the self-powered UPS keeps its queue across the
host's power cycle): the initial read times out, the retry collects the
stale reply, and the loop gives up, so startup fails with "Can't read
inventory information from the UPS". systemd then restarts the driver,
and each failed start abandons further exchanges, degrading the device
until only reseating the USB cable recovers it.

Genuine Modbus exception responses still break out of the loop
immediately: the device answering properly will not improve on a retry.

Signed-off-by: d01 <d01@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retry loop from networkupstools#3414 kept the pre-retry code's LOG_ERR inside the new
loop, so every attempt is logged as an error -- including the ones the
very next retry recovers, which is the exact case the loop was added to
handle.

On a device that needs retries routinely this floods the system log with
errors the driver does not have. Measured on an APC Smart-UPS X1500 in a
degraded state: ~310 LOG_ERR/min while the connection was never closed
once and every read ultimately succeeded -- about 890k lines/day and 42%
of the journal, doubled again by upsdrvctl forwarding the driver's stderr
alongside its own syslog write.

Log attempts with upsdebugx(1), and the error once, after the loop, where
the read has really failed and the connection is about to be closed. Both
lines carry the attempt count. Nothing is logged when exit_flag broke the
loop: the driver is stopping, not failing.

Signed-off-by: d01 <d01@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

A ZIP file with standard source tarball and another tarball with pre-built docs for commit 9544af8 is temporarily available: NUT-tarballs-PR-3571.zip.

@jimklimov jimklimov added bug APC modbus service/daemon start/stop General subject for starting and stopping NUT daemons (drivers, server, monitor); also BG/FG/Debug Shutdowns and overrides and battery level triggers Issues and PRs about system shutdown, especially if battery charge/runtime remaining is involved Connection stability issues Issues about driver<->device and/or networked connections (upsd<->upsmon...) going AWOL over time impacts-release-2.8.5 Issues reported against NUT release 2.8.5 (maybe vanilla or with minor packaging tweaks) labels Aug 14, 2026
@jimklimov jimklimov added this to the 2.8.6 milestone Aug 14, 2026
@jimklimov jimklimov added the AI For good or bad, machine tools are upon us. Humans are still the responsible ones. label Aug 14, 2026
@AppVeyorBot

Copy link
Copy Markdown

Build nut 2.8.5.5063-master completed (commit f377459c07 by @d01)

]

Signed-off-by: Jim Klimov <jimklimov+nut@gmail.com>
@jimklimov

Copy link
Copy Markdown
Member

Great catches, thanks!

@AppVeyorBot

Copy link
Copy Markdown

Build nut 2.8.5.5064-master completed (commit 955241d6e6 by @jimklimov)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI For good or bad, machine tools are upon us. Humans are still the responsible ones. APC bug Connection stability issues Issues about driver<->device and/or networked connections (upsd<->upsmon...) going AWOL over time impacts-release-2.8.5 Issues reported against NUT release 2.8.5 (maybe vanilla or with minor packaging tweaks) modbus service/daemon start/stop General subject for starting and stopping NUT daemons (drivers, server, monitor); also BG/FG/Debug Shutdowns and overrides and battery level triggers Issues and PRs about system shutdown, especially if battery charge/runtime remaining is involved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants