Getting started with Yokan

Installing Yokan

Yokan can be installed using Spack as follows.

spack install mochi-yokan +bedrock

The +bedrock variant will enable Bedrock support, which will be useful for spinning up a Yokan server without having to write code. The spack info mochi-yokan command can be used to show the list of variants available. Many variants refer to database backend types. They are disabled by default. In this tutorial we will use the map backend, which is natively available in Yokan, but feel free to try these tutorials with other backends!

In the following sections, the code can be compiled and linked against the yokan-server, yokan-client and libraries, which can be found either by calling find_package(yokan) in CMake, or pkg-config --libs --cflags yokan-server (respectively yokan-client) with PkgConfig.

Instantiating a Yokan provider

Yokan adopts the typical Mochi microservice architecture (used for instance in the Margo microservice template), with a server library providing the microservice’s provider implementation, and a client library providing access to its capabilities (e.g., putting and getting key/value pairs). A provider is what holds a database, hence the first thing we need to do is instantiate a provider.

Since we have enabled Bedrock support, let’s take advantage of that and write a config.json file for Bedrock to use (if you are not familiar with Bedrock, I highly recommand you to read the Bedrock section. Using Bedrock will save you development time since it allows you to bootstrap a Mochi service using a JSON file instead of writing code).

{
    "libraries": {
        "yokan": "libyokan-bedrock-module.so"
    },
    "providers": [
        {
            "name" : "my_yokan_provider",
            "type" : "yokan",
            "provider_id" : 42,
            "pool" : "__primary__",
            "config" : {
                "database": {
                    "type": "map",
                    "config": {}
                }
            },
            "dependencies" : {}
        }
    ]
}

We can now give this config file to Bedrock as follows.

$ bedrock na+sm -c config.json
[2021-10-14 10:16:17.529] [info] [yokan] YOKAN provider registration done
[2021-10-14 10:16:17.530] [info] Bedrock daemon now running at na+sm://8551-0

We now have a Yokan provider running, with a provider id of 42, managing a “map” database, that is, an in-memory map data structure.

If you need to create a provider in C (either because you don’t want to use Bedrock or because you want your provider to be embedded into an existing application), the following code shows how to do that.

server.c (show/hide)

#include <assert.h>
#include <stdio.h>
#include <margo.h>
#include <yokan/server.h>

int main(int argc, char** argv)
{
    margo_instance_id mid = margo_init("na+sm", MARGO_SERVER_MODE, 0, 0);
    assert(mid);

    hg_addr_t my_address;
    margo_addr_self(mid, &my_address);
    char addr_str[128];
    size_t addr_str_size = 128;
    margo_addr_to_string(mid, addr_str, &addr_str_size, my_address);
    margo_addr_free(mid,my_address);

    margo_set_log_level(mid, MARGO_LOG_INFO);
    margo_info(mid, "Server running at address %s", addr_str);

    const char* config = "{\"database\":{\"type\":\"map\",\"config\":{}}}";

    yk_return_t ret = yk_provider_register(
        mid, 42, config, NULL, YOKAN_PROVIDER_IGNORE);
    assert(ret == YOKAN_SUCCESS);

    margo_wait_for_finalize(mid);

    return 0;
}

Interacting with the database via the client interface

Now we can use the client library to create a client object, create a database handle, and start interacting with our database.

client.c (show/hide)

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <margo.h>
#include <yokan/client.h>
#include <yokan/database.h>

int main(int argc, char** argv)
{
    if(argc != 3) {
        fprintf(stderr, "Usage: %s <address> <provider id>\n", argv[0]);
        exit(-1);
    }
    margo_instance_id mid = margo_init("na+sm", MARGO_CLIENT_MODE, 0, 0);
    assert(mid);

    uint16_t provider_id = atoi(argv[2]);
    hg_addr_t server_addr = HG_ADDR_NULL;
    hg_return_t hret = margo_addr_lookup(mid, argv[1], &server_addr);
    assert(hret == HG_SUCCESS);

    yk_return_t ret;
    yk_client_t client = YOKAN_CLIENT_NULL;

    ret = yk_client_init(mid, &client);
    assert(ret == YOKAN_SUCCESS);

    yk_database_handle_t db_handle = YOKAN_DATABASE_HANDLE_NULL;
    ret = yk_database_handle_create(
        client, server_addr, provider_id, true, &db_handle);
    assert(ret == YOKAN_SUCCESS);

    const char* key = "matthieu";
    const char* value_in = "dorier";

    ret = yk_put(db_handle, YOKAN_MODE_DEFAULT,
                 key, strlen(key), value_in, strlen(value_in));
    assert(ret == YOKAN_SUCCESS);

    char value_out[128];
    size_t value_out_size = 128;
    ret = yk_get(db_handle, YOKAN_MODE_DEFAULT,
                 key, strlen(key), value_out, &value_out_size);
    assert(ret == YOKAN_SUCCESS);

    assert(strcmp(value_in, value_out) == 0);

    ret = yk_database_handle_release(db_handle);
    assert(ret == YOKAN_SUCCESS);

    ret = yk_client_finalize(client);
    assert(ret == YOKAN_SUCCESS);

    margo_finalize(mid);

    return 0;
}

The client is created using yk_client_init. We then create a yk_database_handle_t. This handle is the object that will let us interact with the database.

As an example of using the client API, we show the use of yk_put and yk_get to respectively put and get a key/value pair from the database. These functions will be detailed more in the next tutorial.

yk_database_handle_release should be called to destroy the database handle. yk_client_finalize is then used to finalize the client.

This program can be called as follows (changing the address as needed).

$ ./client na+sm://8972-0 42