Skip to content

File plugin.cpp

File List > plugins > udp_network_backend > plugin.cpp

Go to the documentation of this file

#include "plugin.hpp"

#include "illixr/network/udp_packet.hpp"

#include 
#include 

using namespace ILLIXR;

udp_network_backend::udp_network_backend(const std::string& name_, phonebook* pb_)
    : plugin(name_, pb_)
    , switchboard_{pb_->lookup_impl<switchboard>()} {
    // read environment variables
    if (switchboard_->get_env_char("ILLIXR_SERVER_IP")) {
        server_ip_ = switchboard_->get_env_char("ILLIXR_SERVER_IP");
        spdlog::get("illixr")->info("[udp_network_backend] Using server IP {}", server_ip_);
    } else if (switchboard_->get_env_char("ILLIXR_UDP_SERVER_IP")) {
        server_ip_ = switchboard_->get_env_char("ILLIXR_UDP_SERVER_IP");
        spdlog::get("illixr")->info("[udp_network_backend] Using UDP server IP {}", server_ip_);
    } else if (switchboard_->get_env_char("ILLIXR_TCP_SERVER_IP")) {
        server_ip_ = switchboard_->get_env_char("ILLIXR_TCP_SERVER_IP");
        spdlog::get("illixr")->info("[udp_network_backend] Using TCP/UDP server IP {}", server_ip_);
    }

    if (switchboard_->get_env_char("ILLIXR_UDP_SERVER_PORT")) {
        server_port_ = std::stoi(switchboard_->get_env_char("ILLIXR_UDP_SERVER_PORT"));
        spdlog::get("illixr")->info("[udp_network_backend] Using UDP server port {}", server_port_);
    }

    if (switchboard_->get_env_char("ILLIXR_CLIENT_IP")) {
        client_ip_ = switchboard_->get_env_char("ILLIXR_CLIENT_IP");
        spdlog::get("illixr")->info("[udp_network_backend] Using client IP {}", client_ip_);
    } else if (switchboard_->get_env_char("ILLIXR_UDP_CLIENT_IP")) {
        client_ip_ = switchboard_->get_env_char("ILLIXR_UDP_CLIENT_IP");
        spdlog::get("illixr")->info("[udp_network_backend] Using UDP client IP {}", client_ip_);
    } else if (switchboard_->get_env_char("ILLIXR_TCP_CLIENT_IP")) {
        client_ip_ = switchboard_->get_env_char("ILLIXR_TCP_CLIENT_IP");
        spdlog::get("illixr")->info("[udp_network_backend] Using TCP/UDP client IP {}", client_ip_);
    }

    if (switchboard_->get_env_char("ILLIXR_UDP_CLIENT_PORT")) {
        client_port_ = std::stoi(switchboard_->get_env_char("ILLIXR_UDP_CLIENT_PORT"));
        spdlog::get("illixr")->info("[udp_network_backend] Using UDP client port {}", client_port_);
    }

    if (switchboard_->get_env_char("ILLIXR_IS_CLIENT")) {
        is_client_ = std::stoi(switchboard_->get_env_char("ILLIXR_IS_CLIENT"));
        spdlog::get("illixr")->info("[udp_network_backend] Is client {}", is_client_);
    } else {
        is_client_ = 0;
    }

    if (is_client_) {
        client = true;
        // Android needs to hand the threads differently
#ifdef __ANDROID__
        auto* socket = new network::UDPSocket();
        socket->socket_set_reuseaddr();
        socket->socket_set_receive_timeout(100);
        // Always bind so the OS assigns a local port, making the client reachable for
        // server -> client datagrams (e.g., future round-trip topics).  If
        // ILLIXR_UDP_CLIENT_PORT is set, bind to that specific port; otherwise bind to
        // port 0 and let the OS assign an ephemeral port.
        if (!client_ip_.empty())
            socket->socket_bind(client_ip_, client_port_);
        else if (client_port_ != 0)
            socket->socket_bind(client_port_);
        else
            socket->socket_bind(0);

        socket->set_peer(server_ip_, server_port_);
        peer_socket_ = socket;

        spdlog::get("illixr")->info("[udp_network_backend] Connecting to {}:{}", server_ip_, server_port_);
        // UDP is connectionless � set_peer() is sufficient; no connect() needed
        spdlog::get("illixr")->info("[udp_network_backend] Client ready");
#else
        io_thread_ = std::thread([this]() {
            start_client();
        });

        // wait till we are connected
        while (!ready_) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }

#endif
    } else {
        client = false;
        // Android needs to handl;e the threads differently
#ifdef __ANDROID__
        auto* socket = new network::UDPSocket();
        socket->socket_set_reuseaddr();
        socket->socket_set_receive_timeout(100);
        socket->socket_bind(server_ip_, server_port_);

        spdlog::get("illixr")->info("[udp_network_backend] Listening on UDP port {}", server_port_);

        peer_socket_ = socket;
#else
        io_thread_ = std::thread([this]() {
            start_server();
        });

        while (!ready_) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }

#endif
    }
}

#ifdef __ANDROID__
void udp_network_backend::start() {
    plugin::start();
    io_thread_ = std::thread([this]() {
        read_loop(peer_socket_);
    });
}
#else

void udp_network_backend::start_client() {
    auto* socket = new network::UDPSocket();
    socket->socket_set_reuseaddr();
    socket->socket_set_receive_timeout(100);
    // Always bind so the OS assigns a local port, making the client reachable for
    // server -> client datagrams (e.g., future round-trip topics).  If
    // ILLIXR_UDP_CLIENT_PORT is set, bind to that specific port; otherwise bind to
    // port 0 and let the OS assign an ephemeral port.
    if (!client_ip_.empty())
        socket->socket_bind(client_ip_, client_port_);
    else if (client_port_ != 0)
        socket->socket_bind(client_port_);
    else
        socket->socket_bind(0);
    socket->set_peer(server_ip_, server_port_);
    peer_socket_ = socket;

    spdlog::get("illixr")->info("[udp_network_backend] Connecting to {}:{}", server_ip_, server_port_);
    // UDP is connectionless - set_peer() is sufficient; no connect() needed
    spdlog::get("illixr")->info("[udp_network_backend] Client ready");

    ready_ = true;
    read_loop(socket);
}

void udp_network_backend::start_server() {
    auto* socket = new network::UDPSocket();
    socket->socket_set_reuseaddr();
    socket->socket_set_receive_timeout(100);
    socket->socket_bind(server_ip_, server_port_);

    // If the client's address is already known from the environment (rather than
    // needing to be learned dynamically), set it now so we can send to the client
    // even if it never sends us anything first. Otherwise, read_loop() falls back
    // to learning the peer from the first datagram it receives.
    if (!client_ip_.empty() && client_port_ != 0) {
        socket->set_peer(client_ip_, client_port_);
        spdlog::get("illixr")->info("[udp_network_backend] Pre-configured peer {}:{}", client_ip_, client_port_);
    }

    spdlog::get("illixr")->info("[udp_network_backend] Listening on UDP port {}", server_port_);

    peer_socket_ = socket;
    ready_       = true;
    read_loop(socket);
}
#endif

void udp_network_backend::read_loop(network::UDPSocket* socket) {
    std::string buffer;
    while (running_) {
        // Each recvfrom() returns exactly one datagram - no partial-read reassembly needed
        sockaddr_in src_addr{};
        std::string packet = socket->read_data(&src_addr);
        if (packet.empty())
            continue;

        // The client already has its peer set from construction, so has_peer() is
        // true and this is a no-op there. The server, however, does not know the
        // client's address until it receives a datagram from it, since UDP is
        // connectionless; learn the peer here so topic_send/send_control have
        // somewhere to reply to.
        if (!socket->has_peer())
            socket->set_peer(src_addr);

        receive_packet(std::move(packet));
    }
}

void udp_network_backend::topic_create(std::string topic_name, network::topic_config& config) {
    networked_topics_.push_back(topic_name);
    networked_topics_configs_[topic_name] = config;
    spdlog::get("illixr")->info("[udp_network_backend] topic_create: {}", topic_name);
    // Notify the peer of the new topic and its serialization method, mirroring
    // the TCP backend's illixr_control handshake.  Since UDP is unreliable we
    // send it a few times to reduce the chance of loss before data arrives.
    if (peer_socket_ != nullptr && peer_socket_->has_peer()) {
        std::string serialization =
            (config.serialization_method == network::topic_config::SerializationMethod::BOOST) ? "BOOST" : "PROTOBUF";
        std::string ctrl_message = "create_topic" + topic_name + delimiter_ + serialization;

        for (int i = 0; i < 3; ++i)
            send_control(ctrl_message);
    } else {
        spdlog::get("illixr")->error("[udp_network_backend]: ERROR socket: {}  has_peer: {}",
                                     (peer_socket_ == nullptr) ? "null" : "valid",
                                     peer_socket_ != nullptr && peer_socket_->has_peer());
    }
}

bool udp_network_backend::is_topic_networked(std::string topic_name) {
    return std::find(networked_topics_.begin(), networked_topics_.end(), topic_name) != networked_topics_.end();
}

void udp_network_backend::topic_send(std::string topic_name, std::string&& message) {
    if (!is_topic_networked(topic_name)) {
        spdlog::get("illixr")->warn("[udp_network_backend] topic_send: {} not networked", topic_name);
        return;
    }
    // Packet format: total_length(4) | topic_name_length(4) | topic_name | message
    auto     topic_name_length = static_cast<uint32_t>(topic_name.size());
    uint32_t total_length      = 8u + topic_name_length + static_cast<uint32_t>(message.size());

    std::string packet;
    packet.reserve(total_length);
    packet.append(reinterpret_cast<const char*>(&total_length), 4);
    packet.append(reinterpret_cast<const char*>(&topic_name_length), 4);
    packet.append(topic_name);
    packet.append(message);

    send_packet(std::move(packet));
}

void udp_network_backend::send_packet(std::string&& packet) {
    if (peer_socket_ == nullptr || !peer_socket_->has_peer()) {
        spdlog::get("illixr")->warn("[udp_network_backend] Cannot send UDP packet: peer unavailable");
        return;
    }
    // Preserve the original single-datagram wire format for tracking/control.
    // Large payloads such as video frames belong on the TCP backend.
    if (packet.size() > network::max_udp_payload_bytes) {
        spdlog::get("illixr")->warn("[udp_network_backend] UDP message exceeds the datagram limit ({} bytes); use TCP",
                                    packet.size());
        return;
    }
    std::lock_guard<std::mutex> lock{send_mutex_};
    if (!peer_socket_->write_data(packet)) {
        spdlog::get("illixr")->warn("[udp_network_backend] Failed to send UDP packet");
    }
}

// Keep wire-length validation separate from socket/lifecycle handling. This helper
// decodes one complete datagram with the original topic envelope.
void udp_network_backend::receive_packet(std::string&& packet) {
    network::udp_packet_view decoded;
    if (!network::decode_udp_packet(packet, decoded)) {
        spdlog::get("illixr")->warn("[udp_network_backend] Invalid UDP envelope ({} bytes), dropping", packet.size());
        return;
    }
    const std::string topic_name{decoded.topic};
    std::vector<char> message(decoded.payload.begin(), decoded.payload.end());
    topic_receive(topic_name, message);
}

// Helper function to queue a received message into the corresponding topic
void udp_network_backend::topic_receive(const std::string& topic_name, std::vector<char>& message) {
    if (topic_name == "illixr_control") {
        std::string message_str(message.begin(), message.end());
        if (message_str.find("create_topic") == 0) {
            size_t d_pos = message_str.find(delimiter_);
            assert(d_pos != std::string::npos);
            std::string l_topic_name  = message_str.substr(12, d_pos - 12);
            std::string serialization = message_str.substr(d_pos + 1);
            networked_topics_.push_back(l_topic_name);
            network::topic_config cfg;
            cfg.serialization_method = (serialization == "BOOST") ? network::topic_config::SerializationMethod::BOOST
                                                                  : network::topic_config::SerializationMethod::PROTOBUF;
            cfg.transport_method     = network::topic_config::TransportMethod::UDP;
            networked_topics_configs_[l_topic_name] = cfg;
            spdlog::get("illixr")->info("[udp_network_backend] Received create_topic for {}", l_topic_name);
        }
        return;
    }
    if (!switchboard_->topic_exists(topic_name)) {
        return;
    }
    auto config = networked_topics_configs_.find(topic_name);
    if (config == networked_topics_configs_.end()) {
        // A producer may start before the UDP peer is known, so its unreliable
        // create_topic announcement can be missed. Data topics used by this
        // backend default to Boost serialization; accepting that default keeps
        // startup order from preventing tracking updates from arriving.
        network::topic_config fallback;
        fallback.serialization_method = network::topic_config::SerializationMethod::BOOST;
        fallback.transport_method     = network::topic_config::TransportMethod::UDP;
        config                        = networked_topics_configs_.emplace(topic_name, fallback).first;
        if (std::find(networked_topics_.begin(), networked_topics_.end(), topic_name) == networked_topics_.end()) {
            networked_topics_.push_back(topic_name);
        }
        spdlog::get("illixr")->info("[udp_network_backend] Inferred Boost/UDP configuration for {}", topic_name);
    }
    switchboard_->get_topic(topic_name).deserialize_and_put(message, config->second);
}

void udp_network_backend::stop() {
    // Wake the timeout-bounded receive loop, then keep the socket alive until
    // its only reader has returned.
    if (!running_.exchange(false)) {
        return;
    }
    if (peer_socket_ != nullptr) {
        peer_socket_->socket_shutdown();
    }
    if (io_thread_.joinable() && io_thread_.get_id() != std::this_thread::get_id()) {
        io_thread_.join();
    }
    delete peer_socket_;
    peer_socket_ = nullptr;
    plugin::stop();
}

udp_network_backend::~udp_network_backend() {
    stop();
}

void udp_network_backend::send_control(const std::string& message) {
    // Send a control datagram directly, bypassing the is_topic_networked guard
    // since illixr_control is not registered as a networked topic.
    auto     topic_name_length = static_cast<uint32_t>(std::strlen("illixr_control"));
    uint32_t total_length      = 8u + topic_name_length + static_cast<uint32_t>(message.size());

    std::string packet;
    packet.reserve(total_length);
    packet.append(reinterpret_cast<const char*>(&total_length), 4);
    packet.append(reinterpret_cast<const char*>(&topic_name_length), 4);
    packet.append("illixr_control");
    packet.append(message);

    send_packet(std::move(packet));
}

extern "C" MY_EXPORT_API plugin* this_plugin_factory(phonebook* pb) {
    auto* obj = new udp_network_backend("udp_network_backend", pb);
    // The runtime owns the plugin returned by this factory. Register a non-owning
    // service alias so the phonebook does not try to delete the same object again.
    pb->register_impl<network::udp_backend>(
        std::shared_ptr<network::udp_backend>(static_cast<network::udp_backend*>(obj), [](network::udp_backend*) { }));
    return obj;
}