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.