DUNE-DAQ
DUNE Trigger and Data Acquisition software
|
The goal of the conffwk package is to provide user-friendly API to access data from the configuration database. There are two layers of such API which can be seen by user:
This page describes basics which a user should know to generate DAL from the database schema, to get data from database, to receive notification on their change, to create new data or to modify existing data using generated DAL.
Sections 1, 2 and 3 are needed for those, who develops own schema and wants to generate DAL. Section 4 explain basics of error handling to be known by any user of DAL. Section 5 explain how to get data using DAL. Section 6 explains how to create or to modify data using DAL. Section 7 explains how to receive notification in case of data changes. The possibility to plug-in user algorithms to the generated DAL is described in section 8. Section 9 explains how to extend generated classes inserting user code.
It is possible to develop a new schema in case one needs to describe own configuration data which cannot be described by existing schemes. The development of the schema can be done using the OKS Schema Editor. There is a choice to extend the existing schemes or to develop own schema from scratch. To extend existing schema it is necessary to start the editor with it (which cannot be modified) and then to create the schema that can be updated, e.g.:
The editor window with loaded schema will appear. Then it is possible to create own schema and to define own classes. To create a new schema from scratch, one just needs to run the editor without parameters and to create a new schema. For more information on the OKS schema editor, the OKS schema capabilities and exporting schema into different formats see OKS documentation. After the schema development is finished, it is necessary to save the schema into xml file and add it to the sources of some package. Such schema file will be used for the database data access library generation described by next section.
A DAL is generated by the genconffwk utility. It uses OKS schema files as input and produces:
The command line parameters of genconffwk utility are listed below:
To generate a DAL it is necessary to provide name of the schema file. By default the DAL is generated for all classes contained in the schema files, otherwise one should provide names of required classes via –classes parameter. It is recommended to use unique namespace for each generated DAL to avoid possible problems when several DALs are used by one application.
It is possible to reuse already generated DALs. In this case one should to provide a list of directories containing information about already generated DALs via --include-dirs
parameter, then to specify list of schema files and optionally to specify list of names of classes to be generated. It is expected such schema files use include statement for base schema files. The DAL is generated only for classes contained in the explicitly mentioned schema files, the classes from included files are ignored.
See CMake for ATLAS TDAQ Software TWiki page
The tdaq_generate_dal()
command should be used to generate C++ and Java code from OKS schema files. The full signature looks like this:
The command will generate the C++ and/or Java code and put the names of the generated files in the variables passed in the CPP_OUTPUT
and JAVA_OUTPUT
arguments respectively. Here is a simple use case:
This generates the C++ code for the classes in the schema file, using the namespace MyNamespace
and referring to previously declared classes from the dal package. The header files will be installed into include/DFConfiguration
. Note that the user doesn't have to know the names of the generated files. They are simply passed on to the library that uses them. In a similar way, the Java sources can be generated (typically using the PACKAGE
argument to specify the generated package name. You can also generate the C++ and Java with a single command. The Java sources should be passed to the tdaq_add_jar()
command.
If you want to build the generated dump command for this schema, specify the DUMP_OUTPUT
variable which will contain the source file for the dump command. You can pass that to tdaq_add_executable()
.
For both C++ and Java based libraries the DAL option should be specified when generated source files are used. This ensures that certain race conditions during the generation and the usage of the files are properly handled. The most common use case of having a single DAL and one or more libraries/jars using it is covered in this case.
If you are in the unusual situation that you generate more than one DAL in the same package, you have to do a bit more work: First pass the TARGET target_name parameter to tdaq_generate_dal()
. It will uses this name instead of a default to represent the generation of the source files. Then use the add_dependencies()
command to explicitly add dependencies between the DAL generation target and any consumers of the generated files. The internal target is the name you give, prefixes with DAL_.
You may not encounter any problems during development at all, especially if you don't do a highly parallel build. However, during the nightly builds we use all the cores of the machines we are running on, and if there are users in other packages of your library, the probability of race conditions increases.
For each OKS class appropriate DAL classes are generated:
_Impl
; the static methods to get existent or to create new objects of the class are in the class with suffix _Helper
; an appropriate inheritance is used between interfaces.For each direct attribute and relationship defined for OKS class the appropriate methods are generated. Such methods have the same names as the names of the attributes and the relationships in the database with get_
and set_
prefixes. The database attribute types are mapped to appropriate C++ and Java types. The multi-value attributes are mapped to std::vector
of attribute type in C++ and to array of attribute type in Java. The database relationships are mapped to methods returning pointer or std::vector
of pointers to objects of referenced class in C++ and similarly an object or array of objects in Java.
Additionally, for each class there are methods to get object's class name and object identity as they are defined in the database.
For C++ two std::ostream
operators are generated for each class:
In Java for each class there is generated method print() which prints out full description of the object.
When DAL is generated, any non-alphanumeric characters appeared in the names of classes, attributes, relationships and methods are replaced by underscore symbol, e.g. database attribute "# of c++ lines"
will appear in DAL as "__of_c___lines"
.
Below there is map between OKS attribute types and C++/Java types:
OKS Type | C++ type | Java type |
bool | bool | boolean |
s8 (8-bits signed integer) | int8_t | char |
u8 (8-bits unsigned integer) | uint8_t | byte |
s16 (16-bits signed integer) | int16_t | short |
u16 (16-bits unsigned integer) | uint16_t | short |
s32 (32-bits signed integer) | int32_t | int |
u32 (32-bits unsigned integer) | uint32_t | int |
s64 (64-bits signed integer) | int64_t | long |
u64 (64-bits unsigned integer) | uint64_t | long |
float | float | float |
double | double | double |
date | std::string | java.lang.String |
time | std::string | java.lang.String |
string | std::string | java.lang.String |
enum | std::string | java.lang.String |
class | std::string | java.lang.String |
In C++ the complex values (strings and vectors) are passed by const reference and not by value.s
Methods of C++ and Java conffwk classes throw exceptions in case of errors.
C++ methods either have explicit noexcept specification if they may not throw an exception, or declare exception specification in doxygen documentation. The following exceptions can be thrown:
daq::conffwk::Generic
is used to report most of the problems (bad DB, wrong parameter, plug-in specific, etc.) daq::conffwk::NotFound
the conffwk object accessed by ID is not found, class accessed by name is not found daq::conffwk::DeletedObject
accessing template object that has been deleted (via notification or by the user's code)All above exceptions have common class daq::conffwk::Exception
, that can be used to catch all of them.
The entry point to get data from database is the class Configuration defined in global namespace in C++ and in the conffwk package in Java. Below it is described how to create an object of this class and how to use it to get the database information.
The Configuration
constructors in both Java and C++ languages have single string parameter. If the parameter is an empty string, the constructor will use value of the TDAQ_DB
environment variable. In case if both the constructor parameter and the environment variable are not set, or empty, then the configuration constructor throws daq::conffwk::Generic
in case of C++, or conffwk.SystemException
is thrown in case of Java.
The format of the parameter is "name-of-plugin:plugin-parameters". The plug-in's parameter is optional. If it is non-empty, it is passed to the implementation plug-in constructor.
In C++ the name of plug-in is converted into name of the shared library by adding prefix lib
and suffix .so, e.g. "oksconflibs" plug-in name is converted into "liboksconflibs.so". The shared library must be in the path to shared libraries, e.g. in the LD_LIBRARY_PATH environment variable.
In Java the name of plug-in is converted into name of the class in package "plugin-name" with name created from "plugin-name", where 1-st and 4-th characters are converted to upper case and "uration" string is appended (it is so by historical reasons), e.g. "oksconflibs" plug-in name is converted into "oksconflibs.OksConfiguration". The CLASSPATH variable has to point to such class or jar file.
For the moment three implementation plug-ins are available:
Below there are examples of the Configuration constructor explicit parameters:
The recommended way is to get plug-in and it's parameter via environment variable. Most of the user's code for applications run by TDAQ's setup should to leave the parameter empty:
In case it the parameter can be also passed via command line, use it as shown below:
Note, to get proper plug-in name and parameter used for configuration initialization (e.g. obtained via TDAQ_DB), use get_impl_spec() method of Configuration class, which is used in above examples for debug reporting.
The Configuration constructor parameters are the same, as in case of C++. In case of problems the exception is thrown. An example is shown below:
Note in case when rdb implementation is used, the partition name of the RDB server can be specified by several ways:
Once an object of the Configuration class is successfully created, it can be used to get configuration data (i.e. objects). Normally to get configuration objects only C++ template methods of the Configuration class and generated Java code should be used. The usage of conffwk layer (i.e. direct usage of objects of ConfigObject class) only makes sense in few packages working with arbitrary database schemes.
For each generated class T two methods can be applied using configuration object:
const T * Configuration::get(const std::string&, bool, bool, unsigned long, const std::vector<std::string> *)
- to read named object; void Configuration::get(std::vector<const T*>&, bool, bool, const std::string&, unsigned long rlevel, const std::vector<std::string> *)
- to read objects of class; The methods looking for single object by identity and methods looking for objects of class in case if the query string is empty are searching objects in the class and all it's subclasses, e.g. if class B is derived from class A, then the second method used for class A returns all objects of class A and all objects of class B, and the first method used for class A is looking for object with given identity in class A and then in class B. The same methods used for class B are only looking for objects of class B.
If the query is non-empty, the methods filling vectors of objects only return objects satisfying the query criteria. A query can be an OKS query string. It can be created by the OKS Data Editor or written by hand as described by the OKS documentation , e.g.:
To read all applications via C++ dal provided by the online software one can write:
To get only run control applications it is possible to use the following code:
Note, to get a named object it is necessary to use template parameter explicitly and to write:
instead of
When the methods parameter init_children
is set to true, all objects referenced by the retrieved objects are also read and initialized (i.e. values of their attributes are read from database and all referenced objects are also recursively read). Otherwise the referenced objects can only be pre-allocated (if they were not already read explicitly) and the actual reading will happen, when the user will apply a method to read values of their attributes or relationships.
When the methods parameter init_object
is set to false, all retrieved objects are only pre-allocated without reading their attributes and relationships. The values of attributes and relationships will actually be read from database implementation, when the user will apply a method to read an attribute or relationship value.
The above two parameters can be used by the user to improve performance. For example, if the parameter init_children
is set to false, the only objects which are really used by the user process will be read from the database. However in this case the database can not be closed (e.g. to free used resources) until the configuration data are used. Also, the actual read from database can happen at an unexpected moment, that can introduce undesired delays.
To read all applications via Java dal provided by the online software one can write:
To get only run control applications it is possible to write (try/catch statements are skipped):
Below there is an example of code to get an application by ID. Note, if object with such ID does not exist, conffwk.NotFoundException exception is thrown.
Once the objects are retrieved, the user can get values of their attributes. A method to read attribute value is created for each attribute of each generated class. It has the following format:
type get_AttributeName() const
- for single-value integer and float numbers; const std::string& get_AttributeName() const
- for single-value string-based attributes; const std::vector<type>& get_AttributeName() const
- for multi-value attributes; type get_AttributeName() const
- for single-value attributes; type[] get_AttributeName() const
- for multi-value attributes. The user can use one or several ways to convert values of all attributes of a C++ or Java type. To do this he/she needs to implement or to use already existing converter class, to create converter object of that class and to pass such object to the Configuration object using method Configuration::register_converter().
In case of C++ such class has to inherit from the template Configuration::AttributeConverter < T > class, where template parameter T defines type of attributes which values need to be converted and to implement virtual method Configuration::AttributeConverter::convert(), that performs the real conversion of attribute values.
delete
on the attribute converter object.In case of Java a converter class has to implement conffwk.AttributeConverter interface defining two methods: the method convert() as in C++ and the method get_class(), which returns class of converted attributes.
Below there is C++ example of user functions converting string and integer attributes:
In the Java an example of Java code is shown below:
The core TDAQ C++ DAL (libdaq-core-dal.so) provides converter daq::core::SubstituteVariables class to substitute configuration parameters in values of string attributes. It's constructor requires Configuration object and Partition object, since they are used to calculate conversion map. In case, if configuration database is reloaded, such parameters have to be reset using reset() method. An example of the C++ code is shown below:
Similar Java dal.jar provides attribute converter in the same way. An example of it's usage is shown below:
Once an object is retrieved, the user can get objects referenced by it. A method is created for each relationship of each generated class. It has the following format:
const class-type * get_RelationshipName() const
- for single-value relationships; const std::vector<const class-type*>& get_RelationshipName() const
- for multi-value relationships; class-type get_RelationshipName() const
- for single-value relationships; class-type[] get_RelationshipName() const
- for multi-value relationships. There are situations when user may need to cast an object from one class to a derived one. To make a down cast for an object of generated class the user should to use the methods of the configuration classes and never use cast supported by the programming languages.
There are situations when some set of objects can belong to different classes, e.g. objects can be of class A or B which is derived from class A. For a down cast the ::Configuration::cast() method must be used. As an example, the code to try and to cast from application to run-control application type is shown below:
To down cast an object of generated Java DAL use appropriate cast() method in generated class. For example, some object of application class can be down casted to the run control application:
In C++ the object of the ::Configuration class should not be destroyed while the DAL is in use. All objects read via template methods are destroyed by the ::Configuration class destructor. The user must never try to modify or to destroy such objects himself.
This section explains how to create a new database file, how to create or remove database data and how to modify existing data.
Any modifications described by this section becomes persistent and visible to others processes only after successful commit operation. If the modification should not be committed (e.g. a modification failed), it is necessary to execute abort operation, e.g. in C++:
Below there is the same example for Java:
To modify or to destroy an object using generated C++ DAL methods described below, it is necessary to have a non-const pointer or reference on the object. However all generated DAL methods return objects as const. To make a change it is necessary to use C++ const_cast
to get non-const pointer or reference.
To create a new database file using C++ it is necessary to build an object of the ::Configuration class only providing name of implementation plug-in:
Similar code for Java is below:
To create a new database data file it is necessary to decide which schema (at least one schema is always required) and optionally others database files will be used. Then it is necessary to provide an absolute name for newly created database file (the user should have write permission or the rdb server must be run in read-write mode under account which has such rights). If rdb implementation is used, it is also necessary to provide server and optionally partition name. After this it is necessary to use create method of the ::Configuration class and check it's return status.
Below there is example for C++ and oks implementation:
For rdb implementation it is similar, but requires rdb server and optionally partition's names:
For Java an example with rdb implementation is shown below:
The included files should exist in advance and be defined either as an absolute path or as a relative path to a token of the TDAQ_DB_PATH variable value.
There are methods in C++ class ::Configuration to add a new include, to remove an existing include or to get list of includes for given database. They are:
bool Configuration::add_include(const std::string& db_name, const std::string& include)
- adds include to the database db_name and returns true in case of success or false if failed; bool Configuration::remove_include(const std::string& db_name, const std::string& include)
- removes an existing include from the database db_name and returns true in case of success or false if failed; bool Configuration::get_includes(const std::string& db_name, std::list<std::string>& includes) const
- fills list of includes by files which are included by the db_name and returns true in case of success or false if failed.Similar methods in Java class conffwk.Configuration are:
void add_include(String db_name, String include)
- adds include to the database db_name or throws exception if failed; void remove_include(String db_name, String include)
- removes an existing include from the database db_name or throws exception if failed; void get_includes(String db_name, String[] includes)
- fills array of includes by files which are included by the db_name or throws exception if failed.This subsection explains how to create and how to destroy database objects.
To create a new object using generated C++ DAL there are two ::Configuration template methods:
const T * Configuration::create(const std::string& at, const std::string& id, bool)
- to create new object of class T with identity id at existing database file with name at; const T * Configuration::create(const ::DalObject& at, const std::string& id, bool)
- to create new object of class T with identity id at a database file where object at is stored.The methods return non-null pointer in case of success or null if failed. The second method is faster since time to search the database file where to put new object is much smaller.
When the init_object
parameter is set to false, then the values of attributes and relationships are not read from implementation (for a newly created object they are set to default values in accordance with the database schema).
An example how to create two new objects of the online dal::Computer class is shown below:
On Java similar methods are genereted in the helper classes. For class T two methods are available:
T create(conffwk.Configuration db, String at, String id)
- to create new object of class T with identity id at existing database file with name at; T create(conffwk.Configuration db, conffwk.DalObject at, String id)
- to create new object of class T with identity id at a database file where object at is stored.The example to create online segment and it's application is shown below::
To destroy an existing object there is template method in the C++ ::Configuration class bool destroy(T& obj). It returns true in case of success and false if failed. See example:
In Java the method void destroy(conffwk.Configuration db) is generated in T.java, e.g.:
Once the objects are retrieved or created, the user can modify values of their attributes. A method to set attribute value is created for each attribute of each generated class. The mapping between C++/Java types and OKS types can be seen in the 3.1. Mapping Between OKS Attribute Types and Programming Languages Types section.
In C++ such method throws daq::conffwk::Exception if failed:
void set_AttributeName(type value)
- for single-value attribute; void set_AttributeName(const std::vector<type>& value)
- for multi-value attribute.In Java such method throws an exception if failed:
void set_AttributeName(type value)
- for single-value attribute; void set_AttributeName(type[] value)
- for multi-value attribute.Once the objects are retrieved or created, the user can modify values of their relationships. A method to set relationship value is created for each relationship of each generated class.
For C++ it has the following format and throws daq::conffwk::Exception if failed:
void set_RelationshipName(const class-type * value)
- for single-value relationships; void set_RelationshipName(const std::vector<const class-type*>& value)
- for multi-value relationships.For Java it has the following format and throws an exception if failed:
void set_RelationshipName(class-type value)
- for single-value relationships; void set_RelationshipName(class-type[] value)
- for multi-value relationships.There are several methods which may make instances of ConfigObject and generated DAL classes be invalid:
In case of ConfigObject there is no simple way to know if an object is valid after above remove_include() or destroy_obj() method call, since by efficiency reasons all methods are redirected to implementation object without checking of it's validity. It is recommended to reinitialize all instances of ConfigObject after above calls.
With objects of generated DAL classes the situation is different. After implementation object destruction a method invoked on corresponding DAL objects will throw daq::conffwk::DeletedObject exception, that can be caught by user code. Also, validity of object can be tested with DalObject::was_removed() method.
An example is shown below for include file removal and exception mechanism for generated DAL objects:
The user application can be notified on changes of the configuration data. To do this user should to implement one or many callback functions (C++) or classes (Java) which will be used when the database changes are committed and to choose which changes in classes and objects should be reported (i.e. to define the subscription criteria ).
The user receives description of information changes in one go via callbacks invoked after commit of database changes. This is more preferred way than individual callback per object or per class since user may want to see all changes at single point. Each callback receives own list of changes in accordance with it's subscription criteria.
The changes are reported as a collection of changes per DAL class. A change per class contains 4 parameters: the class name and the identities of created, modified and removed objects.
To make a subscription it is necessary to make three steps:
To start with any subscription on database changes the user must to implement at least one ::Configuration::notify callback function in C++ or conffwk.Callback interface on Java. Below there are details for C++ and Java subscriptions.
The user has to implement ::Configuration::notify callback. It has the following parameters:
const std::vector<::ConfigurationChange *> & changes
- description of changes void * parameter
- user parameterThe ::ConfigurationChange class is declared in the conffwk/Change.h file and has 4 methods to get name of the class and vectors of created, modified and removed object identities. An example of callback functions is shown below:
The user has to create a class implementing the conffwk.Callback interface. It requires to implement method void process_changes(conffwk.Change[] changes, java.lang.Object parameter). Example below illustrates how to implement notification callback:
The subscription criteria is an object of ::ConfigurationSubscriptionCriteria class in C++ or conffwk.Subscription class in Java. It is used to define lists of classes and objects, which changes will be monitored and reposted to user. If user provides no any class or object, it means subscription on any change and a database modification is reported.
The notification callback is invoked for any changes of class objects including creation of new objects, removing or modification of existing objects.
In C++ to subscribe on any changes in some class the user should to use ::ConfigurationSubscriptionCriteria::add(const std::string&) method. For a class generated by genconffwk the s_class_name attribute can be used, e.g. to subscribe on changes in class dal::Application:
In Java method conffwk.Subscription.add(String class_name) should be used, e.g. to subscribe on changes in class Application it is necessary to write the following code:
When subscription on object changes has done, the notification callback is invoked for any changes of the objects or it's removing.
In C++ to subscribe on object changes notification the user should to use ::ConfigurationSubscriptionCriteria::add(const ::DalObject&), e.g. to subscribe on changes of an object of the Application class:
In Java conffwk.add(DalObject obj) method to be used, e.g.:
To make the actual subscription it is necessary to have a notification callback been implemented and a subscription criteria object. The the method subscribe() to be invoked on the configuration object. For C++ an example is shown below:
For Java above example looks like:
The method unsubscribe() can be used to remove subscription set above. In case of C++ it's parameter is a return value of the subscribe() method (i.e. ::CallbackId value). For Java it's parameter is the subscription object used as parameter of subscribe() method.
By default, the generated classes have one-to-one mapping to database schema and DAL objects directly correspond to the database objects. If user wants to add more algorithms on top of the generated DAL without modification of DAL code by hand, he has possibility to define algorithms on top of the OKS class methods.
When a class method is created, user can add it's implementation for different programming languages. To be taken into account by genconffwk, user has to provide C++ and/or Java implementation. Then he has two possibilities:
The first way is more flexible, but requires more steps when build library. The second way does not require any additional steps when build library, but will require schema modifications to any method's implementation modification.
The OKS methods allow to insert user specific code into generated C++ and Java files and classes. In this case one has to put into method's implementation the code inside special keywords described below:
BEGIN_HEADER_PROLOGUE
and END_HEADER_PROLOGUE
(C++ only) Put code into C++ header file before declaration of class containing given method. BEGIN_HEADER_EPILOGUE
and END_HEADER_EPILOGUE
(C++ only) Put code into C++ header file after declaration of class containing given method. BEGIN_PUBLIC_SECTION
and END_PUBLIC_SECTION
Put code into public section of C++ or Java class containing given method. BEGIN_PRIVATE_SECTION
and END_PRIVATE_SECTION
Put code into private section of C++ or Java class containing given method. BEGIN_MEMBER_INITIALIZER_LIST
and END_MEMBER_INITIALIZER_LIST
Put code into initializer list of C++ or Java class containing given method.The online DAL defines several algorithms (e.g. to find partition, get all applications, to calculate application environment, etc.) and uses first way to implement algorithms. More information can be found in the dal package.