Troubleshooting
HORUS ships four utility scripts: install.sh and uninstall.sh for Linux and macOS, and install.ps1 and uninstall.ps1 for Windows PowerShell (no bash or MSYS2 needed). This page covers upgrading, recovering from a broken installation, and debugging HORUS applications at runtime.
Quick Reference
| Command | Use When | What It Does |
|---|---|---|
horus self update | Move to the latest release | Replaces the binary and refreshes the cached source, both at one tag |
install.sh with HORUS_VERSION | Install or reinstall a specific release | Both halves from that tag; the way to roll back |
install.sh with HORUS_BUILD_FROM_SOURCE=1 | The release binary will not run here | Compiles the same tag locally |
install.ps1 | Install on Windows | Native PowerShell; install.sh under Git Bash cannot unpack the zip |
uninstall.sh | Remove HORUS (Linux/macOS) | Complete removal; --dry-run first |
.\uninstall.ps1 | Remove HORUS (Windows) | Complete removal, native PowerShell |
Several fixes below used to end in "re-run ./install.sh". On its own that
downloads the same release again and installs the same binary, so if the release
is what is broken for you, it changes nothing — which is why the loop felt like
a loop. When you reinstall to fix something, change what you install: pin a
different HORUS_VERSION, or build the tag from source. Both are spelled out
under Manual Recovery.
Quick Diagnostic Steps
When your HORUS application isn't working:
- Check the Monitor: Run
horus monitorto see active nodes, topics, and message flow. The monitor ships as a plugin — if it reports "The monitor plugin is not installed", runhorus install horus-monitorfirst. - Examine Logs: Look for error messages in your terminal output
- Verify Topics: Ensure publisher and subscriber use exact same topic names
- Check Shared Memory: Look in
/dev/shm/horus_default/(or/dev/shm/horus_*/) for stale HORUS memory regions - Test Individually: Run nodes one at a time to isolate the problem
Upgrading HORUS
horus self update # move the CLI and the cached source to the latest release
horus self update --check # report current vs latest, change nothing
Both halves move together: horus self update replaces the binary and refreshes
~/.horus/cache/horus@<version> at the same tag. A binary-only update would
leave your projects compiling against the previous release's headers.
To see what an update would do first:
horus self update --check
It exits 0 whether or not an update is available. A non-zero exit means the check failed — GitHub unreachable or rate-limited — not that you are up to date.
To install a specific release, or to roll back:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
Working on HORUS itself, from a checkout — this builds your tree, and does not touch a release:
git pull
HORUS_LOCAL_SOURCE="$PWD" ./install.sh
Full details: Upgrading HORUS.
Manual Recovery
Use when: Build errors, corrupted cache, installation broken
Quick Steps
# 1. Remove the cached HORUS source — both roots `horus run` searches
rm -rf ~/.horus/cache # what the installer writes: horus@<version> + pre-compiled deps
rm -rf ~/.cache/horus # XDG cache root — searched first by `horus run`
# 2. Reinstall. Pin the tag so you know what you got, rather than resolving
# "latest" again and landing on the same thing.
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
If that reinstalls the same broken binary, the release is the problem, not the cache. Compile the same tag locally instead — same source, your toolchain, your libc:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_BUILD_FROM_SOURCE=1 bash
Or step back to a release that worked:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.3.0 bash
cargo clean only empties target/ in whatever directory you run it in. It has
no effect on an installed HORUS unless you are standing in the source tree the
installer cached, and it is not part of recovering a broken install — removing
~/.horus/cache already discards those artifacts.
When to Use Recovery
Symptoms requiring recovery:
-
Build fails:
error: could not compile `horus_core` -
Corrupted cache:
error: failed to load source for dependency `horus_core` -
Binary doesn't work:
$ horus --help Segmentation fault -
Version mismatches:
error: the package `horus` depends on `horus_core 0.1.0`, but `horus_core 0.1.3` is installed -
Broken after system updates:
- Rust updated
- System libraries changed
- GCC/Clang updated
What Gets Removed
By rm -rf ~/.horus/cache and rm -rf ~/.cache/horus:
- The cached HORUS source tree (
horus@<version>/) and its pre-compiled deps - Cached packages downloaded from the registry
Never removed by the steps above (safe):
~/.horus/config.toml(user settings)~/.horus/installed_versionand~/.horus/install_manifest.toml(the install record — the reinstall rewrites them)~/.config/horus/auth.json(registry auth, on Linux)- Project-local
.horus/directories - Your source code
Full Reset (Nuclear Option)
If the quick steps don't work, do a complete reset:
# Remove everything HORUS-related
rm -rf ~/.horus # source cache (horus@<version>), env files, installed_version, install_manifest.toml
rm -rf ~/.config/horus # registry credentials live here, not under ~/.horus
rm -rf ~/.cache/horus # XDG cache root — searched first by `horus run`
rm -f ~/.cargo/bin/horus ~/.local/bin/horus
# Fresh install, pinned
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
Removing ~/.horus also removes installed_version and
install_manifest.toml, so the next horus new sees no recorded install and
compares clean. That is deliberate here: a fresh install writes both files again.
Installation Issues
Problem: "Rust not installed", or "Rust 1.90 or newer is required"
Rust 1.90 or newer is required; found 1.82.0.
Run: rustup update stable
Only a source build needs a toolchain, and only one at or above the workspace MSRV. The installer reads that floor out of the workspace manifest and stops before compiling, rather than failing several minutes in with a cargo error about a crate you have never heard of.
Solution:
# Install or update Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update stable
# Then try again
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | bash
If you did not mean to build from source, check why the fast path was skipped:
HORUS_BUILD_FROM_SOURCE or HORUS_INSTALL_BRANCH set in your environment, or
a platform with no release asset — see
Prebuilt Binaries.
Problem: "C compiler not found"
Solution:
# Ubuntu/Debian/Raspberry Pi OS - Install ALL required packages
sudo apt update
sudo apt install -y build-essential pkg-config \
libssl-dev libudev-dev libasound2-dev \
libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \
libwayland-dev wayland-protocols libxkbcommon-dev \
libvulkan-dev libfontconfig-dev libfreetype-dev \
libv4l-dev
# Fedora/RHEL
sudo dnf groupinstall "Development Tools"
sudo dnf install -y pkg-config openssl-devel systemd-devel alsa-lib-devel \
libX11-devel libXrandr-devel libXi-devel libXcursor-devel libXinerama-devel \
wayland-devel wayland-protocols-devel libxkbcommon-devel \
vulkan-devel fontconfig-devel freetype-devel \
libv4l-devel
Problem: Build fails with linker errors
error: linking with `cc` failed: exit status: 1
error: could not find native static library `X11`, perhaps an -L flag is missing?
Solution:
# Install ALL missing system libraries (most common cause)
# Ubuntu/Debian/Raspberry Pi OS
sudo apt update
sudo apt install -y build-essential pkg-config \
libssl-dev libudev-dev libasound2-dev \
libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \
libwayland-dev wayland-protocols libxkbcommon-dev \
libvulkan-dev libfontconfig-dev libfreetype-dev \
libv4l-dev
# Or skip the compile entirely — most platforms have a prebuilt binary
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | bash
Update Issues
Problem: "Build failed" during horus self update
self update compiles from source only when the release publishes no asset for
your platform. A failure there is an ordinary build failure — see
Installation Issues for the system libraries — but you
do not have to build at all if a working release exists:
Solution:
# Reinstall a known release binary instead of compiling
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
Problem: horus self update cannot check for updates
x GitHub refused the release check (HTTP 403): API rate limit exceeded.
This is usually the unauthenticated API rate limit, which resets within
the hour. Releases are listed at
https://github.com/softmata/horus/releases/latest
The GitHub API allows 60 unauthenticated requests an hour per IP, which one lab behind one NAT, or a CI matrix, can exhaust. This is reported as an error and exits non-zero on purpose: every failure here — 403, rate limit, DNS, timeout, malformed response — used to be swallowed and printed as if you were up to date.
Solution: wait, or install directly — the installer resolves the tag through a plain redirect, which is not rate limited:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | bash
Problem: "Already up to date" but the binary is broken
horus self update compares versions, so it will not reinstall the version you
already have. Reinstall it explicitly:
Solution:
# Reinstall the same tag, both halves
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
# Or compile that tag locally, if the published binary is what is broken
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_BUILD_FROM_SOURCE=1 bash
Runtime Issues
"horus: command not found"
The binary is in one of two places, and which one depends on what already existed
when you installed: ~/.cargo/bin if you had a Rust toolchain, ~/.local/bin
otherwise. A HORUS_PREFIX install puts it in $HORUS_PREFIX/bin. Adding the
wrong one to PATH fixes nothing, so find it first:
Solution:
# 1. Where is it?
ls -l ~/.cargo/bin/horus ~/.local/bin/horus 2>/dev/null
# The install record names it outright, if you have one:
grep '^binary' ~/.horus/install_manifest.toml
# 2. Put THAT directory on PATH (add the line to ~/.bashrc or ~/.zshrc)
export PATH="$HOME/.local/bin:$PATH" # or $HOME/.cargo/bin
# 3. Reload
source ~/.bashrc # or restart terminal
# 4. Verify
command -v horus
horus --help
fish has no export builtin — export PATH=... is a syntax error there and
config.fish stops loading at that line. Use fish_add_path ~/.local/bin
instead. The installer writes the right form for your shell; this only matters
when you add the line by hand.
If neither file exists, the install did not complete. Run it again and read the last lines it prints — it verifies the binary before reporting success, so a failure is stated rather than implied.
Binary exists but doesn't run
$ horus --help
Segmentation fault
Solution: the binary itself is wrong for this machine, so reinstalling the same one will not help. Compile the tag locally:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_BUILD_FROM_SOURCE=1 bash
For the specific case of version 'GLIBC_2.xx' not found, see
The release binary will not start below —
same remedy, and it is worth confirming that is what you are looking at.
The release binary will not start
horus: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by horus)
The Linux release binaries are dynamically linked against glibc and built to a floor of glibc 2.28 — Debian 10, RHEL 8, Ubuntu 18.04. Anything at or above that runs them.
The message above means the binary you have needs something newer. Three ways
that happens: the release predates the floor being enforced (v0.4.0 and
earlier were built natively on the CI runner and need GLIBC_2.39, which rules
out Raspberry Pi OS, JetPack, Ubuntu 22.04, Debian 12 and RHEL 9); the binary
was copied onto the machine by hand; or the distribution is musl-based, such as
Alpine, and has no glibc at all.
Check what you have:
ldd --version | head -1
Solution: compile the same tag locally. It is the identical source, built against your libc:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_BUILD_FROM_SOURCE=1 bash
A fresh install cannot leave you here silently: the installer runs
horus --version before it reports success, catches exactly this failure, and
prints the source-build command rather than declaring an install that works.
Seeing this message from a binary you placed by hand means that check never ran.
Version mismatch warning
! HORUS version mismatch
CLI version: 0.4.1
Installed HORUS: 0.4.0 (from ~/.horus/install_manifest.toml)
CLI topic ABI: 4
Installed topic ABI: 3
The horus binary and the source tree your projects are compiled against are
different installs. horus new and any dependency-resolving horus run check
this and say so.
It is a warning; the command carries on. The version strings alone are not
proof of anything — 0.4.0 shipped with two different topic ABIs, which is why
topic_version is recorded separately. That line is the one that matters: when
the two topic ABIs differ, nodes built against the installed libraries write
shared-memory segments this CLI's runtime refuses to attach to, and horus run
fails with a topic version error rather than corrupting data.
Reconcile it — move both halves to the same tag:
horus self update
Or install the exact version this CLI came from:
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.1 bash
Use the version the warning printed as CLI version.
To see what is recorded:
horus --version
cat ~/.horus/installed_version
cat ~/.horus/install_manifest.toml
In CI, set HORUS_STRICT_VERSION=1 to turn the warning into a hard failure —
a mismatched toolchain should stop the job rather than warn into a log nobody
reads. Do not set it on a robot: the default is a warning precisely because the
hard failure that preceded it printed a remedy that could not clear it.
An install with no record at all — anything made before these files existed — compares clean and warns about nothing. That is not a bug; it is why the check never fires on an old machine.
Windows: unzip: command not found
* Checksum verified
install.sh: unzip: command not found
install.sh under Git Bash gets this far and stops. unzip is not part of Git
for Windows' bundled MSYS2 set, and the tar that is there is GNU tar, which
cannot read a zip at all.
Solution: use the Windows installer, which does not go near a shell:
irm https://github.com/softmata/horus/raw/main/install.ps1 | iex
install.sh now falls back to Windows' own tar.exe (present since Windows 10
1803) when it can find it, and tells you where the download is when it cannot —
but install.ps1 is the supported Windows path. See
Windows (Native).
Version mismatch errors
error: the package `horus` depends on `horus_core 0.1.0`,
but `horus_core 0.1.3` is installed
Why this happens:
- You updated the
horusCLI to a new version - Your project's
.horus/directory still has cached dependencies from the old version - The cached
Cargo.lockreferences incompatible library versions
Option 1: horus run --clean (recommended - fast & easy)
# Clean cached build artifacts and dependencies
horus run --clean
# This clears .horus/cache/, .horus/bin/ and any top-level target/,
# forcing a fresh build with the new version.
# Note: it does NOT remove .horus/target/ — see Option 2 for that.
Alternative Solutions:
Option 2: Manual cleanup
# Remove the entire .horus directory
rm -rf .horus/
# Next run will rebuild from scratch
horus run
Option 3: Reinstall HORUS itself (for persistent issues)
# Only needed if --clean doesn't work. This replaces the global install —
# binary and cached source — with one known tag.
rm -rf ~/.horus/cache ~/.cache/horus
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
This is a different problem from the version mismatch warning HORUS prints about itself. The error above comes from cargo, about your project's cached dependencies.
For multiple projects:
# Clean all projects in your workspace
find ~/your-projects -type d -name ".horus" -exec rm -rf {}/target/ \;
"HORUS source not found" (Rust projects)
Error: HORUS source not found. This can happen after running 'horus clean -a'.
To fix this, either:
1. Re-run the install script: ./install.sh (or curl the installer)
2. Set HORUS_SOURCE environment variable to your HORUS source directory
3. Clone HORUS to ~/softmata/horus or ~/horus
Solution:
# Option 0: reinstall — this is what repopulates the cache, and it needs no clone
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
# Option 1: Set HORUS_SOURCE (recommended for non-standard installations)
export HORUS_SOURCE=/path/to/horus
echo 'export HORUS_SOURCE=/path/to/horus' >> ~/.bashrc
# Option 2: Install HORUS to a standard location
# The CLI checks these paths automatically, in order:
# - /horus
# - ~/softmata/horus
# - ~/horus
# - /opt/horus
# - /usr/local/horus
#
# It also falls back to the installer's source cache, which has two roots
# (the exact-version directory is preferred, then any horus@* tree):
# - ~/.cache/horus/horus@<version> (XDG: $XDG_CACHE_HOME/horus, or ~/Library/Caches/horus on macOS)
# - ~/.horus/cache/horus@<version> (what install.sh actually writes)
# Verify HORUS source is found
horus build
Why this happens:
horus runneeds to find HORUS core libraries for Rust compilation- It auto-detects standard installation paths
- For custom installations, set
$HORUS_SOURCE
Topic Creation Errors
Symptom: Application crashes on startup with:
Error: Failed to create `Topic<MyMessage>`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value'
Common Causes:
-
Stale Shared Memory from Previous Run
- HORUS uses the
/dev/shm/horus_<namespace>/directory for communication (horus_defaultunlessHORUS_NAMESPACEis set) - If your app crashes, these files persist
Fix: Clean shared memory:
# Remove all HORUS shared memory (scans every horus_* namespace) horus clean --shm - HORUS uses the
-
Insufficient Permissions on
/dev/shmFix: Check permissions:
ls -la /dev/shm/horus_default/topics/ # Should show your user as owner # If not, remove with sudo sudo rm -rf /dev/shm/horus_default/ # Fix permissions (if needed) sudo chmod 1777 /dev/shm -
Disk Space Full on
/dev/shmFix: Check available space:
df -h /dev/shm -
Conflicting Topic Names
- Two Topics with same name but different types
Fix: Use unique topic names:
// BAD: Same name, different types let topic1: Topic<f32> = Topic::new("data")?; let topic2: Topic<String> = Topic::new("data")?; // CONFLICT! // GOOD: Different names let topic1: Topic<f32> = Topic::new("sensor_data")?; let topic2: Topic<String> = Topic::new("status_data")?;
General Code Fix:
// Topic names become file paths on the underlying shared memory system.
// Use simple, descriptive names with dots (not slashes):
let topic = Topic::new("sensor_data")?;
let topic = Topic::new("camera.front.raw")?;
Topic name rejected as an absolute path
Symptom: Topic::new fails and the message repeats the name back:
SHM region name must be relative, got absolute path "/sensors/camera"
Cause: A leading slash. Topic names are relative — they are joined onto the shared
memory topics directory, and a name beginning with / would discard that directory and
put the region at an arbitrary path, so the name validator rejects it on every platform
(along with .. and . components).
Interior slashes are accepted everywhere. On Linux they nest a directory under
/dev/shm/horus_<namespace>/topics/ (the namespace is default unless HORUS_NAMESPACE
is set); on macOS the whole name is hashed into one flat shm_open object, so the
separator never reaches the filesystem.
Topic: "sensors.camera" → /dev/shm/horus_default/topics/sensors.camera
Topic: "sensors/camera" → /dev/shm/horus_default/topics/sensors/camera
Topic: "/sensors/camera" → rejected: SHM region name must be relative
Fix: Drop the leading slash. Prefer dots, which is the convention the rest of HORUS follows:
// REJECTED - a leading slash is an absolute path
let topic: Topic<f32> = Topic::new("/sensors/camera")?;
// ACCEPTED, but off-convention: the fan-out lock filename flattens `/` to `_`,
// so "sensors/camera" and "sensors_camera" share one lock file
let topic: Topic<f32> = Topic::new("sensors/camera")?;
// RECOMMENDED
let topic: Topic<f32> = Topic::new("sensors.camera")?;
let topic: Topic<Twist> = Topic::new("robot.cmd_vel")?;
Coming from ROS? ROS topic names are absolute (/sensor/lidar). HORUS names are
relative, and use dots. See Topic Naming for details.
Topic Not Found / No Messages Received
Symptom: Subscriber node never receives messages even though publisher is sending.
// recv() always returns None
if let Some(data) = self.data_sub.recv() {
println!("Got data"); // Never prints
}
Common Causes:
-
Topic Name Mismatch (Typo)
- This is the #1 cause
Fix: Verify exact topic names:
// Publisher let pub_topic: Topic<f32> = Topic::new("sensor_data")?; // Note: sensor_data // Subscriber (TYPO! Missing underscore) let sub_topic: Topic<f32> = Topic::new("sensordata")?; // CORRECT: let sub_topic: Topic<f32> = Topic::new("sensor_data")?; // Exact matchDebug with Monitor:
horus monitorCheck the "Topics" section to see active topic names.
-
Publisher Hasn't Sent Yet
- Subscriber starts before publisher sends first message
- This is normal! First
recv()will returnNone
Fix: Check multiple ticks:
impl Node for SubscriberNode { fn tick(&mut self) { if let Some(msg) = self.topic.recv() { // Process message } else { // No message yet - this is OK on first few ticks } } } -
Expecting
.order()to sequence them- Subscriber runs before publisher in the same tick
.order()does not sequence main-loop nodes once any node has published or subscribed — the scheduler rebuilds its graph from the topics and stops reading the order numbers. What sequences a publisher and a subscriber is the subscription itself, so this usually means the two are not actually connected: check that both use the identical topic name and message type.A subscriber that legitimately reads whatever is latest should call
read_latest()rather than relying on ordering.
"Type mismatch on topic ..."
A type mismatch is not a cause of silence — it fails loudly, at construction,
before recv() is ever reached. It gets its own section because it is easy to
mistake for a delivery problem.
Symptom: Topic::new() returns an error on startup:
Communication error: Failed to create topic 'data': type mismatch. Existing type 'f32', attempted 'f64'.
Two processes opened the same topic with different message types.
Fix: use distinct topic names for different message types.
Cause: A topic's message type is fixed by whichever process creates it first. Opening the same name with a different type is rejected, so you get an error rather than zero messages.
Fix: Use the same type on both sides, or pick distinct topic names:
// Publisher
let pub_topic: Topic<f32> = Topic::new("data")?;
pub_topic.send(3.14);
// Subscriber (WRONG TYPE) — Topic::new returns Err here
let sub_topic: Topic<f64> = Topic::new("data")?; // f64 != f32
// CORRECT:
let sub_topic: Topic<f32> = Topic::new("data")?; // Same type
Application Hangs / Deadlock
Symptom: Your app starts but freezes with no error messages.
Starting application...
[Nodes initialized]
[Application freezes - no output]
Common Causes:
-
Infinite Loop in
tick()// BAD: Never returns! fn tick(&mut self) { loop { // Process data } } // GOOD: Tick returns after work fn tick(&mut self) { self.process_data(); // Return naturally — scheduler calls tick() again next frame } -
Blocking Operations in
tick()// BAD: Blocks scheduler fn tick(&mut self) { std::thread::sleep(Duration::from_secs(10)); // Blocks everything! } // GOOD: Use tick counter for delays fn tick(&mut self) { self.tick_count += 1; // Execute every 10 ticks (~100ms at the default 100 Hz tick rate) if self.tick_count % 10 == 0 { self.slow_operation(); } } -
Waiting Forever for Messages
// BAD: Blocking wait fn tick(&mut self) { while self.data_sub.recv().is_none() { // Infinite loop if no messages! } } // GOOD: Non-blocking receive fn tick(&mut self) { if let Some(data) = self.data_sub.recv() { // Process data } // Continue even if no message } -
Debug with Logging
fn tick(&mut self) { hlog!(debug, "Tick started"); // Your code here hlog!(debug, "Tick completed"); }If you see "Tick started" but never "Tick completed", the hang is in your code.
Not a hang: a cycle in the pub/sub graph. Node A subscribing to what node B
publishes while node B subscribes to what node A publishes does not freeze anything —
recv() never blocks, so no node waits on another. The scheduler's topological sort
cannot order the cycle, so it prints WARNING: Dependency graph error: ... Falling back to sequential. and ticks every node one at a time in .order() order. What you lose is
parallel dispatch, not liveness.
Messages Silently Dropped
Symptom: Publisher sends messages but subscriber never receives them, and no error is reported.
Cause: send() is lossy. The usual cause is a full ring buffer — the subscriber isn't draining it fast enough. When the ring is full, send() retries briefly (one immediate retry, then 64 spins and 4 yields) and then drops the message, incrementing the topic's drop counter.
Message size alone rarely causes drops: serialized payloads larger than 4KB are spilled to a shared TensorPool automatically, and a payload that still exceeds the current slot (default 8KB) triggers an automatic slot grow followed by a retry.
A second cause, which dropped_count() cannot see: on a broadcast POD topic
(PodShm — a fixed-size message with more than one subscriber) the producer never
fails to send. It overwrites the oldest slot instead. A consumer that falls a full
ring behind is lapped: it detects the overwrite, resumes roughly half a ring back
from the head, and the messages in between are gone. This loss happens on the
receive side, so the publisher's dropped_count() stays at 0 throughout. If
messages are vanishing on a multi-subscriber topic while the drop counter reads
zero, this is why — the fix is the same as below, drain faster or publish slower.
Fix:
1. Confirm the drops and count them:
// dropped_count() = send() calls that gave up after the bounded retry
if self.topic.dropped_count() > 0 {
hlog!(warn, "{} messages dropped on '{}'",
self.topic.dropped_count(), self.topic.name());
}
This counter is per-process and lives in the publishing process — it is not
written to shared memory, so read it from the node that owns the topic rather
than looking for it in horus monitor. (From Python: topic.stats()["send_failures"].)
2. Let the subscriber drain the ring:
Give the subscriber a tick rate at least as high as the publisher's:
scheduler.add(PublisherNode::new()?).order(0).rate(50_u64.hz()).done()?;
scheduler.add(SubscriberNode::new()?).order(1).rate(200_u64.hz()).done()?;
Or drain everything queued on each tick instead of a single message:
fn tick(&mut self) {
while let Some(msg) = self.topic.recv() {
self.process(msg);
}
}
3. Give a bursty publisher a deeper ring:
// Default capacity is one 4KB page worth of slots, clamped to 16..=1024
// and rounded up to a power of two. Ask for more explicitly:
let topic: Topic<SensorData> = Topic::with_capacity("sensor_data", 1024, None)?;
4. For messages you cannot afford to lose on a point-to-point topic, use send_blocking():
(On a broadcast topic it does not send at all: it returns Err(SendBlockingError::NoBackpressure) without attempting the write. There, the fix is a faster consumer or a deeper ring — reaching for send_blocking() makes it worse.)
use std::time::Duration;
// Waits for space instead of dropping. Err(Timeout) if the ring stayed
// full for the whole timeout; Err(NoBackpressure) if this topic turned out
// to be broadcast-backed, in which case nothing was sent.
if let Err(e) = self.cmd_topic.send_blocking(cmd, Duration::from_millis(5)) {
hlog!(error, "cmd_vel send timed out: {}", e);
}
If you suspect an oversized payload instead, message size rarely causes drops (see above), but you can check it directly:
use std::mem::size_of;
// POD messages always fit (slot = size_of::<T>())
// Non-POD messages are serialized; payloads over 4KB are spilled to a TensorPool
println!("Message size: {} bytes", size_of::<MyMessage>());
A #[repr(C)] #[derive(Clone, Copy)] POD message skips serialization on the hot path — the ring slot is a memcpy of T — but Clone + Serialize + Deserialize are still required trait bounds on every message type, so the derives stay. If you keep a serde message with a large fixed array, note that serde only derives for arrays up to length 32 — longer ones need serde_arrays (add serde_arrays = "0.2" to your dependencies):
#[derive(Clone, Serialize, Deserialize)]
pub struct LargeMessage {
#[serde(with = "serde_arrays")]
pub data: [u8; 4096], // Fixed 4KB
}
Build and Compilation Issues
"unresolved import" or "cannot find type in this scope"
Symptom: Code won't compile, missing types or functions.
Fix: Add HORUS to horus.toml:
[dependencies]
horus = "*"
Import the prelude:
use horus::prelude::*; // Provides Twist, LaserScan, CmdVel, etc.
horus::prelude::* re-exports the standard robotics messages (CmdVel, Twist, Imu,
LaserScan, BatteryState, …), so horus is the only dependency you need — there is no
separate message crate to add.
"trait bound ... is not satisfied"
Symptom: Compiler says your message doesn't implement required traits.
Fix: Add required derives:
// Clone + Serialize + Deserialize are required on every message
// (Debug is optional, but worth having)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyMessage {
pub field: f32,
}
Performance Issues
Problem: Slow builds
Solution:
# Use release mode (optimized)
horus run --release
Problem: Large disk usage
Solution:
# Clean old cargo cache
cargo clean
# Remove unused dependencies
cargo install cargo-cache
cargo cache --autoclean
Problem: Large .horus/target/ directory (Rust projects)
Why this happens:
- Cargo stores build artifacts in
.horus/target/ - Debug builds are unoptimized and larger
- Incremental compilation caches intermediate files
Solution:
# Clean build artifacts in current project
# (`horus run --clean` does not reclaim it — but `horus clean` does,
# removing both `target/` and `.horus/target/`)
rm -rf .horus/target/
# Regular cleanup (if working on multiple projects)
find . -type d -name ".horus" -exec rm -rf {}/target/ \;
# Add to .gitignore (already included in horus new templates)
echo ".horus/target/" >> .gitignore
Disk usage typical sizes:
.horus/Cargo.toml: ~266 bytes.horus/target/debug/: ~10-100 MB (incremental builds).horus/target/release/: ~5-50 MB (optimized, no debug symbols)
Best practices:
.horus/target/is in.gitignoreby default- Clean periodically if disk space is limited
- Only
.horus/Cargo.tomland.horus/Cargo.lockare needed for rebuild
Using the Monitor to Debug
The monitor is your best debugging tool for runtime issues.
Starting the Monitor:
# Terminal 1: Run your application
horus run
# Terminal 2: Start monitor
horus monitor
The monitor ships as a plugin. If horus monitor reports "The monitor plugin is not installed", install it first with horus install horus-monitor.
Monitor Features:
1. Nodes Tab:
- Shows all running nodes
- Displays node state (Running, Error, Stopped)
- Shows tick count and timing
- Highlights nodes that aren't ticking (stuck)
2. Topics Tab:
- Lists all active topics
- Shows message types
- Displays publisher/subscriber counts
- 0 publishers = no one is sending
- 0 subscribers = no one is listening
3. Metrics Tab:
- IPC Latency: Communication time (should be <1µs)
- Tick Duration: How long each node takes
- Message Counts: Total sent/received
- If sent > 0 but received = 0, subscriber issue
- If sent = 0, publisher issue
4. Graph Tab:
- Visual node graph
- Shows message flow between nodes
- Disconnected nodes = topic mismatch
Debug Workflow:
1. Check Nodes tab
-> All nodes Running? (If Error, check logs)
2. Check Topics tab
-> Topics exist? (If no, topic name typo)
-> Publishers > 0? (If no, publisher not working)
-> Subscribers > 0? (If no, subscriber not created)
3. Check Metrics tab
-> Messages sent > 0? (If no, publisher not sending)
-> Messages received > 0? (If no, subscriber not receiving)
-> IPC latency sane? (If >1ms, system issue)
4. Check Graph tab
-> Nodes connected? (If no, topic name mismatch)
Example Debug Session:
# Problem: Subscriber not receiving messages
# Monitor shows:
# Nodes: SensorNode (Running), DisplayNode (Running)
# Topics: "sensor_data" (1 pub, 0 sub) <-- AHA!
# Issue: No subscribers!
# Fix: Check DisplayNode - likely wrong topic name
Reading Log Output
Log Levels
HORUS nodes can log at different severity levels:
fn tick(&mut self) {
hlog!(debug, "Detailed info for debugging");
hlog!(info, "Normal informational message");
hlog!(warn, "Something unusual happened");
hlog!(error, "Something went wrong!");
}
Log Format
Console output uses ANSI-colored formatting:
[INFO] [SensorNode] Sensor initialized
│ │ │
│ │ └─ Message
│ └─ Node name
└─ Log level (INFO, WARN, ERROR, DEBUG)
Timestamps are included in the shared memory log buffer (visible in the monitor), formatted as HH:MM:SS.mmm.
Common Patterns and Anti-Patterns
[OK] DO: Check recv() for None
fn tick(&mut self) {
if let Some(msg) = self.topic.recv() {
// Process message
}
// No message? That's OK, just continue
}
[FAIL] DON'T: Unwrap recv()
fn tick(&mut self) {
let msg = self.topic.recv().unwrap(); // PANIC if no message!
}
[OK] DO: Use Result for errors
impl Node for MyNode {
fn init(&mut self) -> Result<()> {
if self.sensor.is_broken() {
return Err(Error::node("MyNode", "Sensor initialization failed"));
}
Ok(())
}
}
[FAIL] DON'T: panic!() in nodes
fn init(&mut self) -> Result<()> {
if self.sensor.is_broken() {
panic!("Sensor broken"); // DON'T DO THIS
}
Ok(())
}
[OK] DO: Keep tick() fast
fn tick(&mut self) {
// Quick operations only
let data = self.sensor.read_cached();
self.topic.send(data);
}
[FAIL] DON'T: Block in tick()
fn tick(&mut self) {
thread::sleep(Duration::from_millis(100)); // Blocks everything!
let data = self.network.fetch(); // Network I/O blocks!
}
Best Practices
Regular Maintenance
Weekly (active development):
horus self update --check # is there anything new?
horus self update # take it
After system updates: nothing, if you installed a release binary — a new Rust or GCC does not touch it. After a source build, a toolchain change can leave stale artifacts:
rm -rf ~/.horus/cache ~/.cache/horus
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_BUILD_FROM_SOURCE=1 bash
CI/CD Integration
Pin the version. A CI job that resolves "latest" changes what it tests whenever a release is cut, and the failure lands on whoever pushed next:
# In CI pipeline
curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
export HORUS_STRICT_VERSION=1 # a CLI/library mismatch fails the job
horus --version
||-retrying the installer is not worth writing: it verifies the binary before
it reports success, so a run that exits 0 has already proved the CLI starts.
Debugging Workflow
-
First: Check horus works
horus --version && horus --help -
If the CLI and libraries disagree: reconcile them
horus self update -
If errors persist: reinstall a known tag
rm -rf ~/.horus/cache ~/.cache/horus curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash
Getting Help
If you're still having issues:
-
Reinstall a known release:
rm -rf ~/.horus/cache ~/.cache/horus curl -fsSL https://github.com/softmata/horus/raw/main/install.sh | HORUS_VERSION=v0.4.0 bash -
Add Debug Logging:
// Add hlog!(debug, ...) in your nodes to trace execution hlog!(debug, "Node state: {:?}", self.state); -
Test with Minimal Example:
- Strip down to simplest possible code
- Add complexity back one piece at a time
- Identify what causes the error
-
Check System Resources:
# Check available shared memory df -h /dev/shm # Check HORUS files ls -lh /dev/shm/horus_default/topics/ # Clean if needed (scans every horus_* namespace) horus clean --shm -
Report the issue:
- GitHub: https://github.com/softmata/horus/issues
- Include: full error message, minimal code example, OS and platform
- And
horus --versionpluscat ~/.horus/install_manifest.toml— thecommitfield in there identifies the exact tree you are running, which a version string cannot
Next Steps
- Installation - First-time installation guide
- CLI Reference - All horus commands
- Examples - Working code examples
- Performance - Optimization tips
- Testing - Test your nodes to prevent runtime errors