Writing a Bedrock module

Note

As of Bedrock 0.15.0, a Bedrock module can only be written in C++, and you will need to enable at least the C++17 standard with your compiler.

If you have programmed your own Mochi component, writing a module to make it usable with Bedrock is really not difficult. Such a module consists of a single dynamic library (.so) that can be implemented as show in the example bellow.

/*
 * (C) 2020 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <bedrock/AbstractComponent.hpp>
#include <iostream>

struct MyProvider {};

class MyComponent : public bedrock::AbstractComponent {

    std::unique_ptr<MyProvider> m_provider;

    public:

    MyComponent()
    : m_provider{std::make_unique<MyProvider>()} {}

    static std::vector<bedrock::Dependency>
        GetDependencies(const bedrock::ComponentArgs& args) {
        (void)args;
        std::vector<bedrock::Dependency> deps = {
            { "pool", "pool", true, false, true },
            { "kv_store", "yokan", true, true, false }
        };
        return deps;
    }

    static std::shared_ptr<bedrock::AbstractComponent>
        Register(const bedrock::ComponentArgs& args) {
            std::cout << "Registering a MyComponent" << std::endl;
            std::cout << " -> mid = " << (void*)args.engine.get_margo_instance() << std::endl;
            std::cout << " -> provider id = " << args.provider_id << std::endl;
            std::cout << " -> config = " << args.config << std::endl;
            std::cout << " -> name = " << args.name << std::endl;
            std::cout << " -> tags = ";
            for(auto& t : args.tags) std::cout << t << " ";
            std::cout << std::endl;
            auto pool_it = args.dependencies.find("pool");
            auto pool = pool_it->second[0]->getHandle<thallium::pool>();
            return std::make_shared<MyComponent>();
    }

    void* getHandle() override {
        return static_cast<void*>(m_provider.get());
    }

    /* optional */
    std::string getConfig() override {
        return "{}";
    }

    /* optional */
    void changeDependency(
            const std::string& dep_name,
            const bedrock::NamedDependencyList& dependencies) override {
        throw bedrock::Exception{"Operation not supported"};
    }

    /* optional */
    void migrate(
            const std::string& dest_addr,
            uint16_t dest_component_id,
            const std::string& options_json,
            bool remove_source) override {
        throw bedrock::Exception{"Operation not supported"};
    }

    /* optional */
    void snapshot(
            const std::string& dest_path,
            const std::string& options_json,
            bool remove_source) override {
        throw bedrock::Exception{"Operation not supported"};
    }

    /* optional */
    void restore(
            const std::string& src_path,
            const char* options_json) override {
        throw bedrock::Exception{"Operation not supported"};
    }
};

BEDROCK_REGISTER_COMPONENT_TYPE(my_module, MyComponent)

You must compile your module as a dynamic library and link it against the bedrock::module-api CMake target (or use pkg-config to lookup the flags for the bedrock-module-api package). If you provide your Mochi component as a Spack package, you will want to add a dependency on mochi-bedrock-module-api.

The following explains in more detail how such a module works. We assume that you have a MyProvider structure (or type definition) representing an instance of provider for your Mochi component.

The Bedrock component presents itself in the form of a class (MyComponent, here) inheriting from bedrock::AbstractComponent. This class must be registered (in a .cpp file) with the BEDROCK_REGISTER_COMPONENT_TYPE macro so that Bedrock can find it upon loading the library.

Component dependencies

The class must provide two static functions: GetDependencies and Register. The GetDependencies function will be called with an instance of bedrock::ComponentArgs, and should return a vector of bedrock::Dependency. The latter is defined as follows.

struct ComponentArgs {
    std::string              name;         // name of the component
    thallium::engine         engine;       // thallium engine
    uint16_t                 provider_id;  // provider id
    std::vector<std::string> tags;         // Tags
    std::string              config;       // JSON configuration
    ResolvedDependencyMap    dependencies; // dependencies
};

It provides the name of the component, the thallium engine of the process, the provider ID of the component to instantiate, a list of tags, and a configuration string which is guaranteed to be valid JSON. The dependencies field is empty when GetDependencies is called.

The purpose of the GetDependencies function is to tell Bedrock what dependencies your component needs, given the information (such as configuration, tags, name, etc.) it was provided with.

The bedrock::Dependency structure is defined as follows.

struct Dependency {
    std::string name;
    std::string type;
    bool        is_required;
    bool        is_array;
    bool        is_updatable;
};

The name is the name by which the dependency is known in the “dependencies” section of a component, in a Bedrock configuration. The type is the type of dependency, which can be either “pool”, “xstream”, or the name of a Mochi component (e.g. “warabi”, “yokan”, etc.) if the dependency should be a provider handle or a provider instance.

The is_required field indicates whether the dependency is required. The is_array field indicates whether more than one dependency may be specified for this dependency name. The is_updatable field indicates whether the dependency can be updated via a call to changeDependency.

Given the above dependency declarations for our module, a valid provider instantiation in the JSON document might look like the following.

{
     "libraries" : [
         "path/to/libbedrock-my-module.so",
         "libyokan-bedrock-module.so"
     ],
     "providers" : [
         {
             "name" : "MyProvider",
             "type" : "my_module",
             "provider_id" : 42,
             "config" : {},
             "dependencies" : {
                 "pool" : "my_pool",
                 "kv_store" : [ "yokan:33@tcp://localhost:1234", "other_db@local" ]
             }
         },
         ...
     ]
}

The libraries section must contain the dynamic library to load for your module. To be valid (1) the current process has an Argobots pool called “my_pool”, (2) there should exist a Yokan provider with provider ID 33 at tcp://localhost:1234, and (3) there should be another Yokan provider local to the current process and named “other_db”.

Component instantiation

One GetDependencies has been called and Bedrock has looked up the dependencies, the Register static function is called. This time, the dependencies field of the bedrock::ComponentArgs has been filled. This field is of type :code:ResolvedDependencyMap`, which is defined as a map from dependency names to a NamedDependencyList object. A NamedDependencyList is itself a vector of shared pointers to a NamedDependency, which is defined in bedrock/NamedDependency.hpp in the mochi-bedrock-module-api package. Such NamedDependency wraps various types objects: Argobots pools and xstreams, as well as Thallium provider_handles, and handles to user-defined Mochi components. The getHandle method may be used to extract the underlying handle of the dependency. For Argobots pools, getHandle<thallium::pool>() should be used. For Argobots execution streams, getHandle<thallium::xstream>() should be used. For Thallium provider handles, getHandle<thallium::provider_handle>() should be used. For direct handles to other local components, getHandle<bedrock::ComponentPtr>() should be used. ComponentPtr is defined as std::shared_ptr<AbstractComponent>.

Component member functions

The component class must provide at least a getHandle() method returning a void* pointer to the underlying handle of the component. It may also provide the following functions: getConfig, changeDependency, migrate, snapshot, and restore. You will find more information about the expected semantics of these functions in the comments of the bedrock/AbstractComponent.hpp header.