Runtime Configuration Repository

The purpose of the Runtime Configuration Repository is to make available runtime configuration parameters to SRTC components. The parameters are a subset of configuration parameters from the Persistent Configuration Repository that pertain to the current deployment.

Note

Currently only a simple file based Runtime Configuration Repository is available. The Persistent Configuration Repository is not yet implemented in any manner in the current release of the RTC Toolkit, version 0.1.0-alpha. The user must therefore interact directly with the underlying files stored by the Runtime Configuration Repository to prepare a component’s configuration. The YAML file format was chosen to make this easier to deal with for now. In addition, FITS files are used for large vectors and matrices.

In the future, the intention is that the Runtime Configuration Repository is backed by a fully fledged distributed system. It will be populated from the Persistent Configuration Repository automatically. The user will therefore normally interact with the Persistent Configuration Repository directly, rather than the Runtime Configuration Repository. The interaction is expected to occur using rtctkConfigTool, which will in fact also use YAML and FITS as an data exchange format. However, this will not be the format of the underlying production Runtime or Persistent Configuration Repositories. The internal YAML format used for the data exchange will also unfortunately be different in the future. However, the overall concept should remain the same.

See section 4.6.2.3 “Configuration Repositories” in the RTC Toolkit Design document for further details about the intended structure of the repositories.

SRTC components must read their configuration parameters from the Runtime Configuration Repository during the initialisation activity when the Init command is received. Certain computed results can also be written to the repository while the component is running.

Data Access

An overview of the API is provided here, which should be enough to become familiar with it and to be able to use it. One should also refer to the API reference documentation for technical details.

Access to the Runtime Configuration Repository should always be performed through an instance of RuntimeRepoApiIf. The RTC Toolkit framework will automatically prepare such an instance when requested from the ServiceContainer, as long as it is correctly configured in the Service Discovery with the runtime_repo_endpoint datapoint. Currently only a file scheme URI is supported for runtime_repo_endpoint, and should point to the directory containing the YAML files for the repository on the local file system, e.g. file:/home/eltdev/repo.

The ServiceContainer is itself passed to the constructor of the user derived RunnableStateMachineLogic class, called BusinessLogic, and is accessible in user code through the attribute m_services. The following is an example of how to retrieve the RuntimeRepoApiIf within the Initialising method of BusinessLogic:

void BusinessLogic::Initialising(componentFramework::StopToken st) {
    auto repository = m_services.Get<RuntimeRepoApiIf>();
    // Can now access the repository with repository ...
}

Datapoint Paths

Configuration parameters are stored in a tree hierarchy as leaf nodes. This can also be thought of as a folder like structure, similar to a file system. The nodes from the root of the tree to a particular leaf node form the components of a path. By adding the ‘/’ character as the path separator between each component, this forms a datapoint path string.

The canonical structure for the path is as follows:

/<component>/{static,dynamic}/<parameter>

Where <component> will typically be the SRTC component instance name and <parameter> can represent a hierarchical sub-path with multiple sub-folders if a deeper hierarchy of configuration parameters is desired for a particular SRTC component, besides the basic grouping into static and dynamic parameters.

Path components must contain only lowercase alpha numeric characters or the underscore, i.e. characters from the set [a-z0-9_].

Note

The canonical path structure is a suggested convention to follow. Reusable components delivered by the RTC Toolkit follow this convention. However, it is not enforced by RuntimeRepoApiIf, except for making sure that the path components only contain characters from the accepted character set. Therefore the user is technically free to choose any path structure desired.

The paths are handled in the API with the DataPointPath class, which is responsible for checking syntactical structure of the path. These DataPointPath as passed to the methods of RuntimeRepoApiIf to identify the specific datapoint to operate on.

Datapoint Creation

Datapoints need to be created before they can be used. Attempting to writing a datapoint the does not exist will throw an exception. A datapoint can be created with the CreateDataPoint method as follows:

RuntimeRepoApiIf& repo = ...
DataPointPath path = "/mycomp/static/param1";
repo.CreateDataPoint(path, "RtcInt32");

The datapoint type to use in the underlying YAML file is explicitly passed as a string to the method. The currently supported types are indicated in the Supported Data Types section. There is an alternative method CreateDataPointV2 that allows to specify the type as a C++ type and optionally give a default value. It can be used as follows:

RuntimeRepoApiIf& repo = ...
DataPointPath path = "/mycomp/static/param1";

// Without a default value:
repo.CreateDataPointV2<int32_t>(path);

// With a default value:
int32_t default_value = 123;
repo.CreateDataPointV2(path, default_value);

Note

The CreateDataPoint method is likely to be deprecated in the future in favour of CreateDataPointV2.

Datapoint Reading

Reading a datapoint can be done with the GetDataPoint method, or the ReadDataPoint method to update an existing variable in place, which may be useful for large vectors and matrices. For example:

RuntimeRepoApiIf& repo = ...
int32_t param1 = repo.GetDataPoint<int32_t>("/mycomp/static/param1"_dppath);

std::vector<float> param2;
repo.ReadDataPoint("/mycomp/static/param2"_dppath, param2);

You will see the _dppath suffix is added to the string representations of the datapoint paths in the example above. This is a shorthand to construct a DataPointPath object from a null terminated character string.

The GetDataPoint and ReadDataPoint methods are blocking, and will only return to the caller once the data has been received. In certain situations it may be better to avoid blocking. To support this, the API provides the SendReadRequest method that takes a Request object as the input argument and returns a Response object, which can be used to eventually synchronise. The Request class is used to represent a read request for one or more datapoints to be fetched from the Runtime Configuration Repository. The Response object is effectively a future that provides a Wait method that will block until the request has been completed. An optional timeout threshold can be given to the Wait method.

This pattern allows a read request to be sent without blocking, some other work can then be performed while the request is completed in the background; and finally the Wait method can be called to synchronise with the background request. Ideally, by the time the Wait method is called, the request that was initially sent would have completed and the Wait method would return immediately without blocking. Otherwise it will block as long as is necessary for the request to complete.

Any processing, which requires some or all of the datapoints sent as part of the read request, must be performed after having invoked the Wait method and it returns without a timeout condition. Otherwise there will be a race condition between the read request and processing code.

The following example code shows how a Request object is prepared, how the request is sent and how the response is handled.

RuntimeRepoApiIf& repo = ...
int32_t param1;
std::vector<float> param2;

// An example callback lambda function that will force all negative values to zero.
auto handler = [](std::vector<float>& buffer) {
        for (auto& value: buffer) {
            if (value < 0.0) {
                value = 0.0;
            }
        }
    };

Request request;
request.Add("/mycomp/static/param1"_dppath, param1);
request.Add("/mycomp/static/param2"_dppath, param2, handler);
auto response = repo.SendReadRequest(request);

// Other processing not requiring param1 or param2 can happen here ...

response.Wait();

// Any processing requiring access to param1 or param2 goes here ...

As can be seen, the Add method is used to add all the datapoints needed to the request. Two alternative invocations are shown, one with a callback handler function and one without. The optional callback allows processing of a single datapoint’s data asynchronously as soon as it arrives. The callback is executed in a different thread than the one that invoked the SendReadRequest method. Therefore care should be taken if accessing any global variables to avoid race conditions.

Warning

Only the data explicitly passed to the callback’s argument should be accessed within the callback, since it is the only datapoint guaranteed to have been delivered when the callback is executed. No other data buffers for any other datapoints should be accessed within the callback function. Any such attempt will result in race conditions and likely data corruption.

In addition, the datapoint buffers that were added to the request with the Add method must not be accessed outside of a callback function once SendReadRequest has been called. Only after Wait returns successfully can the buffers for all these datapoints be accessed.

Datapoint Writing

Writing a new value to a datapoint can be done with the SetDataPoint method, or the WriteDataPoint method to pass a reference to the data instead, which is more optimal for large vectors or matrices. For example:

RuntimeRepoApiIf& repo = ...
repo.SetDataPoint<int32_t>("/mycomp/static/param1"_dppath, 123);

std::vector<float> value2 = {1.2, 3.4, 5.6, 7.8};
repo.WriteDataPoint("/mycomp/static/param2"_dppath, value2);

The SetDataPoint and WriteDataPoint methods are blocking, and will only return to the caller once the data has been sent to the repository.

Similar to the reading methods, a non-blocking option exists with the SendWriteRequest method. It works in an analogous manner to the SendReadRequest method described in the previous Datapoint Reading section. The SendWriteRequest method accepts a Request object and returns a Response object. All datapoints that should be updated must be added to the Request object with the Add method. The Wait method of the Response object should be called to synchronise with the request completion. The call to Wait will block until the datapoints have been successfully sent to the repository. The Wait method can optionally take a timeout argument.

Warning

The buffers of the datapoints added to the request with the Add method must not be modified after SendWriteRequest has been called. Only after the Wait method returns successfully can the datapoint buffers be modified. Modifying the contents before a successful invocation of Wait will result in race conditions and possible data corruption.

The following is an example of using SendWriteRequest:

RuntimeRepoApiIf& repo = ...
int32_t param1 = ...
std::vector<float> param2 = ...

Request request;
request.Add("/mycomp/static/param1"_dppath, param1);
request.Add("/mycomp/static/param2"_dppath, param2);
auto response = repo.SendWriteRequest(request);

// Other processing can happen here, but param1 and param2 must not be changed ...

response.Wait();

// param1 and param2 can be modified again after the Wait call here ...

Datapoint Querying

To check the data type of a datapoint one can use the GetDataPointType method as follows:

RuntimeRepoApiIf& repo = ...
std::string typestr = repo.GetDataPointType("/mycomp/static/param1"_dppath);

This will return the data type as encoded in the underlying YAML files, specifically one of the YAML type name strings indicated in the Supported Data Types section.

The data size, or more specifically the number of elements for a datapoint, is retrieved with the GetDataPointSize method. Note that this will always return the value 1 for basic types such as int32_t or float. For strings the number of characters is returned, i.e. the length of the string. For vectors and matrices the total number of elements is returned.

The following is an example of using GetDataPointSize:

RuntimeRepoApiIf& repo = ...
size_t size = repo.GetDataPointSize("/mycomp/static/param1"_dppath);

It may be necessary to check for the existence of a datapoint. This can be achieved with the DataPointExists method, which will return true if the datapoint exists and false otherwise. For example:

RuntimeRepoApiIf& repo = ...
if (repo.DataPointExists("/mycomp/static/param1"_dppath)) {
    // Can operate on the datapoint here ...
}

There is also a mechanism to query the names of available datapoint paths using the GetChildren method. This method takes a datapoint path and lists all the child nodes under the path. Specifically, it returns a pair of lists. The first list contains the all the datapoints found within the path and the second list contains all the child paths, i.e. sub-folders. The GetChildren method provides a general mechanism to traverse the datapoint path hierarchy. The following shows an example of traversing and printing all datapoint paths available:

void traverse(RuntimeRepoApiIf& repo, DataPointPath path) {
    auto [datapoints, child_paths] = repo.GetChildren(path);
    for (auto& dp_path: datapoints) {
        std::cout << dp_path << std::endl;
    }
    for (auto& child_path: child_paths) {
        traverse(repo, DataPointPath(child_path));
    }
}

RuntimeRepoApiIf& repo = ...
traverse(repo, "/"_dppath);

Datapoint Deletion

Existing datapoints are deleted with the DeleteDataPoint method. For example:

RuntimeRepoApiIf& repo = ...
repo.DeleteDataPoint("/mycomp/static/param1"_dppath);

Supported Data Types

The following is a table of currently supported data types for the Runtime Configuration Repository:

C++ Type

YAML Type Name

bool

RtcBool

int32_t

RtcInt32

int64_t

RtcInt64

float

RtcFloat

double

RtcDouble

std::string

RtcString

std::vector<bool>

RtcVectorBool

std::vector<int32_t>

RtcVectorInt32

std::vector<int64_t>

RtcVectorInt64

std::vector<float>

RtcVectorFloat

std::vector<double>

RtcVectorDouble

std::vector<std::string>

RtcVectorString

MatrixBuffer<bool>

RtcMatrixBool

MatrixBuffer<int32_t>

RtcMatrixInt32

MatrixBuffer<int64_t>

RtcMatrixInt64

MatrixBuffer<float>

RtcMatrixFloat

MatrixBuffer<double>

RtcMatrixDouble

MatrixBuffer<std::string>

RtcMatrixString

The indicated C++ type should be used for declaring the data variables in the code. The YAML type name should be used in the CreateDataPoint method, which indicates the string to use to identify the datapoint’s type within the YAML file itself.

Note

Support for unsigned integer types is pending and will be available in a future release.

File Format

Here we describe the directory layout and YAML format for the underlying files of the Runtime Configuration Repository. The file system directory that contains the repository’s YAML files is called the base path. It is given by the file scheme URI path encoded in the runtime_repo_endpoint configuration parameter in the Service Discovery. Any DataPointPath is then relative to this base path.

The first component encoded in a DataPointPath is treated as the name of a YAML file. Typically this corresponds to a SRTC component instance name. Therefore if DataPointPath starts with /mycomp/..., the corresponding YAML file will be mycomp.yaml. Assuming further that runtime_repo_endpoint was set to file:/home/eltdev/repo, the base path is /home/eltdev/repo in this case, and the complete file system path for the YAML file would be /home/eltdev/repo/mycomp.yaml. The remaining components encoded in a DataPointPath are treated as dictionary keys, for a hierarchy of dictionaries within the YAML file.

Each datapoint within the YAML will have a dictionary of the following mandatory keys:

Key Name

Description

type

This indicates the type of the data stored in value. It must be one of the YAML type names indicated in section Supported Data Types.

value

Stores the actual value of the datapoint. If a matrix is being stored then value contains a list of elements for the matrix in row major layout. As a special case, the value can be a file scheme URI pointing to a file on the local file system. In such a case, the file is treated as a FITS file.

nrows

The number of rows in the matrix. This key is only applicable to matrices and should not be used for any other datapoint types.

ncols

The number of columns in the matrix. This key is only applicable to matrices and should not be used for any other datapoint types.

Note

Using a file URI in a value key is only applicable to numerical vectors and matrices, i.e. the element type must be a boolean, integer or floating-point number. Trying to use a URI for any other type will cause and exception to be thrown.

The following sections show examples of the YAML corresponding to a datapoint path, for various categories of datapoint type.

Scalar Types

Assume we have a basic type, such as an int32_t with value 123, that should be stored in the datapoint path /mycomp/static/param1. The contents of mycomp.yaml should be as follows:

static:
    param1:
        type: RtcInt32
        value: 123

Another example for a floating-point number float with value 5.32 and stored in the datapoint path /mycomp/static/subdir/param2 is:

static:
    subdir:
        param2:
            type: RtcFloat
            value: 5.32

A string with value xy and z and datapoint path /mycomp/static/param3 should be stored as follows:

static:
    param3:
        type: RtcString
        value: "xy and z"

Vector Types

Assume a numerical vector, e.g. std::vector<int32_t>, with value [1, 2, 3, 4] and stored in the datapoint path /mydatatask/static/param1. The contents of the mydatatask.yaml file should be as following:

static:
    param1:
        type: RtcVectorInt32
        value: [1, 2, 3, 4]

An alternative format for the list in the YAML file for the above example is the following:

static:
    param1:
        type: RtcVectorInt32
        value:
            - 1
            - 2
            - 3
            - 4

This alternative format may be particularly useful for a vector of strings. For example:

static:
    param1:
        type: RtcVectorString
        value:
            - foo
            - bar
            - baz

For large numerical vectors it is more convenient to store the data in a FITS file and reference the FITS file from the YAML with a file scheme URI. The data must be stored in the FITS primary array as a 1-D image. It can be stored as either a 1⨯N or N⨯1 pixel image. The following is an example of the YAML using such a reference to a FITS file:

static:
    param1:
        type: RtcVectorFloat
        value: file:/home/eltdev/repo/mydatatask.static.param1.fits

As seen in the example above, the name of the FITS file in the URI is equivalent to the datapoint path with the ‘/’ path separator character replaced with ‘.’ and the .fits suffix appended. This naming convention is applied automatically by the Runtime Configuration Repository when it writes to datapoints and stores the actual data in FITS files. If a user is creating the YAML file manually and using the FITS file reference feature, the name of the FITS file does not have to follow any particular convention. A user is free to choose any name for the FITS file.

Matrix Types

Assume we have a matrix with type MatrixBuffer<double> and it is stored in the datapoint path /mydatatask/static/param1 with the following value:

\[\begin{split}\begin{bmatrix} 1 & 2 & 3\\ 4 & 5 & 6 \end{bmatrix}\end{split}\]

The corresponding mydatatask.yaml YAML should look as follows:

static:
    param1:
        type: RtcMatrixDouble
        value: [1, 2, 3, 4, 5, 6]
        nrows: 2
        ncols: 3

Similarly to vectors, the entries in value can have an alternative format as follows:

static:
    param1:
        type: RtcMatrixDouble
        value:
            - 1
            - 2
            - 3
            - 4
            - 5
            - 6
        nrows: 2
        ncols: 3

For large matrices it is more convenient to store the data in a FITS file and refer to it from the YAML file. This is done in the same manner as for vectors. The following is an example of this for the matrix case:

static:
    param1:
        type: RtcMatrixDouble
        value: file:/home/eltdev/repo/mydatatask.static.param1.fits
        nrows: 2
        ncols: 3

FITS Writing Threshold

The Runtime Configuration Repositroy will store small vectors and matrices directly in the YAML file, while large vectors and matrices, above a certain threshold, will be stored in FITS files instead. The threshold can be controlled at runtime by setting the /fits_write_threshold datapoint. It takes a 64-bit integer and indicates the number of elements above which the vector or matrix will be stored in a FITS file.

Note

Changing the setting will have no effect on existing vector and matrix datapoints already stored in the repository. Only new values written to the repository will take the threshold value under consideration.

By default the Runtime Configuration Repositroy will use a threshold value of 16 elements. The easiest way to change this is to use rtctkConfigTool. As an example, to change the threshold to always write the data to FITS files, one can use the following command:

rtctkConfigTool --repo file:<repository> --path /fits_write_threshold --type RtcInt64 --set --value 0

The <repository> tag in the above command should be replaced with the appropriate file system path where the repository is actually located.

Warning

Make sure that the path passed to the rtctkConfigTool in the --repo argument ends with a ‘/’ character. Otherwise the path will be interpreted incorrectly.

Limitations and Known Issues

The current implementation of the Runtime Configuration Repository is only a simple file based version using YAML and FITS files. This has the following implications:

  • The performance may be currently limited for large vectors and matrices or for large numbers of requests.

  • The repository is not distributed and therefore typically can only be used to run components locally on a single machine.

Note

If the Runtime Configuration Repository location is configured to point to a NFS mounted file system that is shared between all the machines where the SRTC components will be run, this can allow running SRTC components in a distributed manner correctly. However, NFS version 3 or newer must be used in this case. Older versions do not allow correct file based locking to be implemented and will lead to race conditions.

The file based Runtime Configuration Repository uses a file lock for synchronisation. If for any reason a process that still holds the lock dies without releasing it, this will block all other SRTC components trying to read from the Runtime Configuration Repository. The solution is to simply delete the write.lock file found in the repository’s directory where the top level YAML files are located.