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

CommandUse WhenWhat It Does
horus self updateMove to the latest releaseReplaces the binary and refreshes the cached source, both at one tag
install.sh with HORUS_VERSIONInstall or reinstall a specific releaseBoth halves from that tag; the way to roll back
install.sh with HORUS_BUILD_FROM_SOURCE=1The release binary will not run hereCompiles the same tag locally
install.ps1Install on WindowsNative PowerShell; install.sh under Git Bash cannot unpack the zip
uninstall.shRemove HORUS (Linux/macOS)Complete removal; --dry-run first
.\uninstall.ps1Remove HORUS (Windows)Complete removal, native PowerShell
⚠️Re-running the installer is not a repair step

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:

  1. Check the Monitor: Run horus monitor to see active nodes, topics, and message flow. The monitor ships as a plugin — if it reports "The monitor plugin is not installed", run horus install horus-monitor first.
  2. Examine Logs: Look for error messages in your terminal output
  3. Verify Topics: Ensure publisher and subscriber use exact same topic names
  4. Check Shared Memory: Look in /dev/shm/horus_default/ (or /dev/shm/horus_*/) for stale HORUS memory regions
  5. 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` cleans a checkout, not an install

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:

  1. Build fails:

    error: could not compile `horus_core`
    
  2. Corrupted cache:

    error: failed to load source for dependency `horus_core`
    
  3. Binary doesn't work:

    $ horus --help
    Segmentation fault
    
  4. Version mismatches:

    error: the package `horus` depends on `horus_core 0.1.0`,
    but `horus_core 0.1.3` is installed
    
  5. 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_version and ~/.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 horus CLI to a new version
  • Your project's .horus/ directory still has cached dependencies from the old version
  • The cached Cargo.lock references 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 run needs 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:

  1. Stale Shared Memory from Previous Run

    • HORUS uses the /dev/shm/horus_<namespace>/ directory for communication (horus_default unless HORUS_NAMESPACE is set)
    • If your app crashes, these files persist

    Fix: Clean shared memory:

    # Remove all HORUS shared memory (scans every horus_* namespace)
    horus clean --shm
    
  2. Insufficient Permissions on /dev/shm

    Fix: 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
    
  3. Disk Space Full on /dev/shm

    Fix: Check available space:

    df -h /dev/shm
    
  4. 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:

  1. 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 match
    

    Debug with Monitor:

    horus monitor
    

    Check the "Topics" section to see active topic names.

  2. Publisher Hasn't Sent Yet

    • Subscriber starts before publisher sends first message
    • This is normal! First recv() will return None

    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
            }
        }
    }
    
  3. 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:

  1. 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
    }
    
  2. 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();
        }
    }
    
  3. 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
    }
    
  4. 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 .gitignore by default
  • Clean periodically if disk space is limited
  • Only .horus/Cargo.toml and .horus/Cargo.lock are 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

  1. First: Check horus works

    horus --version && horus --help
    
  2. If the CLI and libraries disagree: reconcile them

    horus self update
    

    See Version mismatch warning.

  3. 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:

  1. 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
    
  2. Add Debug Logging:

    // Add hlog!(debug, ...) in your nodes to trace execution
    hlog!(debug, "Node state: {:?}", self.state);
    
  3. Test with Minimal Example:

    • Strip down to simplest possible code
    • Add complexity back one piece at a time
    • Identify what causes the error
  4. 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
    
  5. Report the issue:

    • GitHub: https://github.com/softmata/horus/issues
    • Include: full error message, minimal code example, OS and platform
    • And horus --version plus cat ~/.horus/install_manifest.toml — the commit field in there identifies the exact tree you are running, which a version string cannot

Next Steps