Skip to main content

1. Use Auto-Detection for Subscribers

Unless you have a specific reason, use auto-detection:
use cerulion_core::prelude::*;

// ✅ Good: Auto-detects best transport
let subscriber = Subscriber::create("topic", None)?;

// ⚠️ Only if you need specific behavior
let subscriber = Subscriber::create("topic", Some(true))?;  // Force network
let subscriber = Subscriber::create("topic", Some(false))?; // Force local
Auto-detection (None) allows the subscriber to choose the optimal transport:
  • If a local publisher exists, it uses high-performance local IPC
  • If no local publisher exists, it falls back to network transport
  • If a local publisher appears later, you can enable it with enable_local_subscriber()

2. Start Publishers with Network Disabled

Start with network disabled and enable it only when needed:
use cerulion_core::prelude::*;

// ✅ Good: Start local-only, enable network on demand
let publisher = Publisher::create("topic", false)?;

// Later, enable network when remote subscriber discovered
publisher.enable_network();

// ⚠️ Only if you know remote subscribers exist
let publisher = Publisher::create("topic", true)?;
This saves resources when only local communication is needed. Network publishing consumes CPU and network bandwidth even when no remote subscribers exist.

3. Handle None Gracefully

Handle cases when no message is available:
use cerulion_core::prelude::*;

// ✅ Good: Handle None case and deserialization
if let Ok(Some(bytes)) = subscriber.receive() {
    if let Ok(data) = SensorData::from_bytes(&bytes) {
        process(data);
    }
}

// ❌ Bad: Assumes message always available
let bytes = subscriber.receive()?.unwrap();
let data = SensorData::from_bytes(&bytes).unwrap(); // May panic
The receive() method returns Ok(None) when no message is available. Always handle this case gracefully.

4. Keep Publishers and Subscribers Alive

Publishers and subscribers must remain in scope for the lifetime you need them:
use cerulion_core::prelude::*;

// ✅ Good: Publisher stays alive
let publisher = Publisher::create("topic", true)?;
// ... use publisher ...

// ❌ Bad: Publisher dropped too early
{
    let publisher = Publisher::create("topic", true)?;
} // Publisher dropped here
let subscriber = Subscriber::create("topic", None)?;
// Subscriber won't receive messages from dropped publisher
If a publisher is dropped, local subscribers will stop receiving messages. If a subscriber handle is dropped, the subscription is closed.

5. Use Shared Sessions for Multiple Pub/Sub Pairs

If you have multiple publishers/subscribers, share sessions and nodes to reduce resource overhead:
use cerulion_core::prelude::*;
use std::sync::Arc;

// ✅ Good: Share session and node across multiple pub/sub
let config = zenoh::Config::default();
let session = Arc::new(zenoh::open(config).wait()?);
let node = Arc::new(NodeBuilder::new().create::<ipc::Service>()?);

let pub1 = Publisher::create_with_session("topic1", true, Some(session.clone()), false, Some(node.clone()))?;
let pub2 = Publisher::create_with_session("topic2", true, Some(session.clone()), false, Some(node.clone()))?;
let sub1 = Subscriber::create_with_session("topic1", None, Some(session.clone()), Some(node.clone()))?;
let sub2 = Subscriber::create_with_session("topic2", None, Some(session.clone()), Some(node.clone()))?;
For multiple topics, consider using TopicManager instead, which automatically manages shared sessions and provides centralized control.

6. Use Typed Messages When Possible

Use send_message() for type safety:
use cerulion_core::prelude::*;

// ✅ Good: Use send_message for type safety
struct SensorData {
    temperature: f32,
    timestamp: u64,
}

impl SerializableMessage for SensorData {
    fn to_bytes(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        // Serialization logic
        Ok(vec![])
    }
}

let data = SensorData { temperature: 23.5, timestamp: 1234567890 };
publisher.send_message(&data)?;

// ⚠️ Use send only when necessary (raw bytes, pre-serialized data)
let raw_bytes = vec![0x01, 0x02, 0x03];
publisher.send(&raw_bytes)?;
send_message() provides compile-time type checking and makes your code more maintainable. Use send() only when you have raw bytes or need to send data that doesn’t implement SerializableMessage.

7. Handle Deserialization Errors

Always handle deserialization errors gracefully:
use cerulion_core::prelude::*;

match subscriber.receive() {
    Ok(Some(bytes)) => {
        match SensorData::from_bytes(&bytes) {
            Ok(data) => {
                // Process message
                process(data);
            }
            Err(e) => {
                // Log error but don't crash
                eprintln!("Deserialization error: {}", e);
                // Optionally: log the raw bytes for debugging
            }
        }
    }
    Ok(None) => {
        // No message available
    }
    Err(e) => {
        eprintln!("Receive error: {}", e);
    }
}
Deserialization can fail if:
  • The message format doesn’t match your struct
  • The message is corrupted
  • The message is from a different version of your struct

8. Consider TopicManager for Multiple Topics

If you have multiple publishers/subscribers, consider using TopicManager:
use cerulion_core::prelude::*;

// ✅ Good: Centralized management with TopicManager
let manager = TopicManager::create()?;
let pub1 = manager.register_publisher("topic1", false, false)?;
let pub2 = manager.register_publisher("topic2", false, false)?;
let sub1 = manager.register_subscriber("topic1", None)?;
let sub2 = manager.register_subscriber("topic2", None)?;

// vs. Direct APIs (more verbose, manual session management)
let session = Arc::new(zenoh::open(config).wait()?);
let node = Arc::new(NodeBuilder::new().create::<ipc::Service>()?);
let pub1 = Publisher::create_with_session("topic1", false, Some(session.clone()), false, Some(node.clone()))?;
let pub2 = Publisher::create_with_session("topic2", false, Some(session.clone()), false, Some(node.clone()))?;
// ... more boilerplate
TopicManager provides:
  • Automatic discovery
  • Shared session management
  • Centralized control
  • Less boilerplate code
Use direct Publisher/Subscriber APIs only when you need fine-grained control that TopicManager doesn’t provide.

9. Use Appropriate Polling Strategy

Choose the right polling strategy for your use case:
use cerulion_core::prelude::*;

// For low-latency applications (tight loop)
loop {
    if let Ok(Some(bytes)) = subscriber.receive() {
        // Process message immediately
        process_message(&bytes);
    }
    // No sleep - fastest response time
    std::hint::spin_loop();
}

// For CPU-friendly applications (with sleep)
loop {
    if let Ok(Some(bytes)) = subscriber.receive() {
        process_message(&bytes);
    }
    // Small sleep to avoid busy-waiting
    std::thread::sleep(std::time::Duration::from_millis(1));
}
For high-frequency data, consider removing the sleep. The receive() call is very fast (< 1 μs for local transport), so busy-waiting may be acceptable for real-time applications.

10. Check Network Status When Debugging

If network communication isn’t working, check the network status:
use cerulion_core::prelude::*;

let publisher = Publisher::create("topic", true)?;

// Check if network is actually enabled
if publisher.is_network_enabled() {
    println!("Network publishing is active");
} else {
    println!("Network publishing is disabled");
}

// Check logs for network errors
// Network failures are logged but don't propagate to the caller
Network failures are logged but don’t propagate to the caller. This ensures local communication continues even if the network is down. Check logs if network communication isn’t working.

Next Steps