Internals of xeus

xeus is internally architected around three main components:

_images/xeus_archi.svg
  • The server is the middleware component responsible for receiving and sending messages to the Jupyter client. It is built upon ZeroMQ and handles the concurrency model of the application.

  • The kernel core routes the messages to the appropriate method of the interpreter and does some book-keeping operations such as storing the message and its answer in the history manager, or sending relevant messages to the server.

  • The interpreter provides the interface that kernel authors must implement.

The interpreter and the server are well isolated from each other, only the kernel core can interact with them. The kernel core is also loosely coupled with the server, which makes it easy to replace the server implementation provided by xeus with a custom one.

Server

Public API

The server part of xeus provides a public API made of:

  • xserver.hpp: This file contains the base class xserver, which must be inherited from any class implementing a server. This is the unique entry point into the server component used by the kernel core.

  • xerver_zmq.hpp: This file contains the interface of the default server implementation, that can be used directly or extended in order to override parts of its behavior.

Before we dive into the details of the server implementation, let’s have a look at the public interface:

space xeus

enum class channel
{
    SHELL,
    CONTROL
};

class XEUS_API xserver
{
public:

    using listener = std::function<void(zmq::multipart_t&)>;
    using internal_listener = std::function<zmq::multipart_t(zmq::multipart_t&)>;

    virtual ~xserver() = default;

    xserver(const xserver&) = delete;
    xserver& operator=(const xserver&) = delete;

    xserver(xserver&&) = delete;
    xserver& operator=(xserver&&) = delete;

    xcontrol_messenger& get_control_messenger();

    void send_shell(zmq::multipart_t& message);
    void send_control(zmq::multipart_t& message);
    void send_stdin(zmq::multipart_t& message);
    void publish(zmq::multipart_t& message, channel c);

    void start(zmq::multipart_t& message);
    void abort_queue(const listener& l, long polling_interval);
    void stop();
    void update_config(xconfiguration& config) const;

    void register_shell_listener(const listener& l);
    void register_control_listener(const listener& l);
    void register_stdin_listener(const listener& l);
    void register_internal_listener(const internal_listener& l);

protected:

    xserver() = default;

    void notify_shell_listener(zmq::multipart_t& message);
    void notify_control_listener(zmq::multipart_t& message);
    void notify_stdin_listener(zmq::multipart_t& message);
    zmq::multipart_t notify_internal_listener(zmq::multipart_t& message);

private:

    virtual xcontrol_messenger& get_control_messenger_impl() = 0;

First thing to notice is the xserver class makes use of the Non-Virtual Interface pattern. This allows a clear separation between the client interface (the public methods) and the interface for subclasses (protected non-virtual methods and private virtual methods).

The client interface can be divided into three parts:

  • the API to control the server: this is how you configure, start and stop the server. The related methods are update_config, start, stop and abort_queue. These methods forward to private pure virtual methods that must be implemented in inheriting classes.

  • the API to send message: this is where you decide on which channel you send the message. The related methods are send_shell, send_control, send_stdin and publish. These methods also forward to virtual methods that must be implemented in inheriting classes.

  • the API to register callbacks: the methods register_shell_listener, register_control_listener and register_stdin_listener allow clients (such as the kernel core component) to register functions that will be called when a message is received by the server. This way, the server component is loosely coupled with its clients, it doesn’t need to know anything about them.

The subclass interface contains private virtual methods that must be implemented in inheriting classes to define the behavior of the server, and protected methods to notify the client that a message has been received. This makes inheriting classes independent from the way the xserver class stores and uses the callbacks.

Default implementation

The xserver_zmq class is the default implementation of the server API, its internals are illustrated in the following diagram:

server

The default server is made of three threads communicating through internal ZeroMQ sockets. The main thread is responsible for polling both shell and controller channels. When a message is received on one of these channels, the corresponding callback is invoked. Any code executed in the interpreter will be executed by the main thread. If the publish method is called, the main thread sends a message to the publisher thread.

Having a dedicated thread for publishing messages makes this operation a non-blocking one. When the kernel main thread needs to publish a message, it simply sends it to the publisher thread through an internal socket and continues its execution. The publisher thread will poll its internal socket and forward the messages to the publisher channel.

The last thread is the heartbeat. It is responsible for notifying the client that the kernel is still alive. This is done by sending messages on the heartbeat channel at a regular rate.

The main thread is also connected to the publisher and the heartbeat threads through internal controller channels. These are used to send stop messages to the subthread and allow to stop the kernel in a clean way.