pvDatabaseCPP

Release 4.2 - 2016.01.12

Abstract

This document describes pvDatabaseCPP, which is a framework for implementing a network accessible database of smart memory resident records. Network access is via pvAccess. The data in each record is a top level PVStructure as defined by pvData. The framework includes a complete implementation of ChannelProvider as defined by pvAccess. The framework can be extended in order to create record instances that implements services. The minimum that an extension must provide is a top level PVStructure and a process method.

For more information about EPICS generally, please refer to the home page of the Experimental Physics and Industrial Control System.

Table of Contents


Introduction

Overview

The main purpose of this project to make it easier to implement services that are accessed via pvAccess. This project supplies a complete implementation of the server side of pvAccess. All that a service has to provide is a top level PVStructure and a process method. A service can be run as a main process or can be part of a V3 IOC. Thus services can be developed that interact with V3 records, asynDriver, areaDetector, etc.

A brief description of a pvDatabase is that it is a set of network accessible, smart, memory resident records. Each record has data composed of a top level PVStructure. Each record has a name which is the channelName for pvAccess. A local Channel Provider implements the complete ChannelProvider and Channel interfaces as defined by pvAccess. The local provider provides access to the records in the pvDatabase. This local provider is accessed by the remote pvAccess server. A record is smart because code can be attached to a record, which is accessed via a method named process.

This document describes components that provide the following features:

database
This encapsulates the concept of a database of memory resident smart records. The two main components are:
pvRecord
This encapsulates the concept of a smart record. It can be processed. Changes to field values can be trapped. A record can be locked.
pvDatabase
This is a database of pvRecords. Records can be added and removed from a database.
pvAccess
This is a complete implementation of ChannelProvider and Channel as defined by pvAccess. It is used by the server side of pvAccess to attach to pvRecords.
Main and V3IOC
The pvDatabase can be provided via a Main program or can be part of a V3IOC. In the later case the IOC has both a database of V3 Records and a pvDatabase.

Base classes make it easy to create record instances. The code attached to each record must create the top level PVStructure and the following three methods:

init
This is a method for initializing the support. It returns true if successful and false otherwise.
process
This is what makes a record smart.
destroy
This releases and resources used by the implementation.

Doxygen documentation is available at doxygenDoc

Getting started

The first step is to build pvDatabaseCPP as described in the next section.

A separate project exampleCPP has examples for pvDatabaseCPP and for pvaClientCPP. See it for examples.

Features Required for localChannelProvider

copy and monitor
pvDataCPP provides facilities copy and monitor. This facilities allow a client to access an arbitrary subset of the fields in the top level structure associated with a channel, and to monitor changes in the top level structure. pvDatabaseCPP uses what pvDataCPP provides and has code that associates these facilities with a PVRecord.
PVRecord and PVDatabase
Defined below.
The localChannelProvider itself
The localChannelProvider accesses data from PVRecords. It implements all channel methods except channelRPC, which is implemented by pvAccessCPP.

Features Required for pvRecord

PVDatabase
This holds a set of PVRecords. It has methods to find, add, and remove records.
PVRecord
This, and a set of related interfaces, provides the following:
Access to top level PVStructure
PVRecord is a wrapper on a top level pvStructure.
Record locking
A record can be locked and unlocked. A record must be locked whenever data in the pvStructure is accessed.
Trapping data changes
A client can request to be notified when data in the pvStructure is modified. It can do this on a field by field basis.
A method named process

The process method is called when a pvAccess client requests that a record be processed. If a top level timeStamp field exists, the default process method just sets the timeStamp to the currect time. A service is created by implementing process and providing a top level PVStructure.

Building pvDatabaseCPP

To build pvDatabaseCPP You must provide a file RELEASE.local in directory configure. Thus do the following:

mrk> pwd
/home/hg/pvDatabaseCPP/configure
mrk> cp ExampleRELEASE.local RELEASE.local

Then edit RELEASE.local so that it has the correct location of each product pvDatabaseCPP requires. Than at the top level just execute make:

mrk> cd ..
mrk> pwd
/home/epicsv4/master/pvDatabaseCPP
mrk> make

This builds pvDatabase and also test.

test is a completely separate top, but is also built when make is run in pvDatabaseCPP itself.

iocshell commands

Shell commands are made available via the standard DBD include mechanism provided by iocCore. The following provide EPICS V4 shell commands:

pvAccessCPP
pvaSrv
pvDatabaseCPP

pvDatabaseCPP provides the following iocshell command.

pvdbl
Provides a list of all the pvRecords in database master

In addition any code that implements a PVRecord must implement an ioc command. Look at any of the examples in exampleCPP to see how to implement shell commands.

database

src/database

This Directory has the following files:

pvDatabase.h
This is what is described in this section.
pvDatabase.cpp
The implementation of PVDatabase.
pvRecord.cpp
The implementation of the base class for PVRecord. It can also implement record instances with a process method does nothing. This can be used to create a "dumb" record where all changes are done by clients.

src/special

This directory has the following files:

traceRecord.h
This implements a PVRecord that can set the trace level for another record. See below for a discussion of trace level.

pvDatabase.h

The classes in pvDatabase.h describe a database of memory resident smart records. It describes the following classes:

PVRecord
This provides the methods required by localChannelProvider to implement Channel.
PVRecordField
PVRecordStructure
These wrap PVField and PVStructure so that pvCopy and monitor can be implemented.
PVRecordClient
This is called by anything that accesses PVRecord.
PVListener
This is implemented by anything that wants to trap calls to PVRecord::message.
PVDatabase
This is a database of PVRecords.

Each class is described in a separate subsection.

C++ namespace and typedefs

namespace epics { namespace pvDatabase {

class PVRecord;
typedef std::tr1::shared_ptr<PVRecord> PVRecordPtr;
typedef std::map<epics::pvData::String,PVRecordPtr> PVRecordMap;

class PVRecordField;
typedef std::tr1::shared_ptr<PVRecordField> PVRecordFieldPtr;
typedef std::vector<PVRecordFieldPtr> PVRecordFieldPtrArray;
typedef std::tr1::shared_ptr<PVRecordFieldPtrArray> PVRecordFieldPtrArrayPtr;

class PVRecordStructure;
typedef std::tr1::shared_ptr<PVRecordStructure> PVRecordStructurePtr;

class PVRecordClient;
typedef std::tr1::shared_ptr<PVRecordClient> PVRecordClientPtr;

class PVListener;
typedef std::tr1::shared_ptr<PVListener> PVListenerPtr;

class PVDatabase;
typedef std::tr1::shared_ptr<PVDatabase> PVDatabasePtr;

class PVRecord

NOTES:

PVRecord Methods

class PVRecord
     public std::tr1::enable_shared_from_this<PVRecord>
{
public:
    POINTER_DEFINITIONS(PVRecord);

    virtual bool init() ;
    virtual void start() {}
    virtual void process() {}
    virtual void destroy();

    static PVRecordPtr create(
        std::string const & recordName,
        epics::pvData::PVStructurePtr const & pvStructure);
    virtual ~PVRecord();
    std::string getRecordName();
    PVRecordStructurePtr getPVRecordStructure();
    PVRecordFieldPtr findPVRecordField(
        epics::pvData::PVFieldPtr const & pvField);
    void lock();
    void unlock();
    bool tryLock();
    void lockOtherRecord(PVRecordPtr const & otherRecord);
    bool addPVRecordClient(PVRecordClientPtr const & pvRecordClient);
    bool removePVRecordClient(PVRecordClientPtr const & pvRecordClient);
    void detachClients();
    bool addListener(PVListenerPtr const & pvListener);
    bool removeListener(PVListenerPtr const & pvListener);
    void beginGroupPut();
    void endGroupPut();
    int getTraceLevel();
    void setTraceLevel(int level);
protected:
    PVRecord(
        std::string const & recordName,
        epics::pvData::PVStructurePtr const & pvStructure);
    void initPVRecord();
    epics::pvData::PVStructurePtr getPVStructure();
    PVRecordPtr getPtrSelf()
    {
        return shared_from_this();
    }
private:
...
}

The methods are:

init
Virtual method. Derived classes must implement this method. This method Must call initPVRecord.
start
Virtual method. Optional method for derived class. It is called before record is added to database. The base method does nothing.
process
Virtual method. Derived classes usually implement this method. It implements the semantics for the record. The base implementation does nothing.
destroy
Virtual method. Optional method for derived class. If the derived class implements this it must call the base class destroy method after it has released any resources it uses.
create
Static method to create dumb records, i.e. records with a process method that does nothing. A derived class should have it's own static create method.
~PVRecord
The destructor which must be virtual. A derived class must also have a virtual destructor.
getRecordName
Return the recordName.
getPVRecordStructure
Get the top level PVStructure.
findPVRecordField
Given a PVFieldPtr return the PVRecordFieldPtr for the field.
lock
unlock
Lock and Unlock the record. Any code accessing the data in the record or calling other PVRecord methods must have the record locked.
tryLock
If true then just like lock. If false client can not access record. A client can try to simultaneously hold the lock for more than two records by calling this method. But must be willing to accept failure.
lockOtherRecord
A client that holds the lock for one record can lock one other record. A client must not call this if the client already has the lock for more then one record.
addPVRecordClient
Every client that accesses the record must call this so that the client can be notified when the record is deleted.
removePVRecordClient
Client is no longer accessing the record.
detachClients
Ask all clients to detach from the record
addListener
Add a PVListener. This must be called before calling pvRecordField.addListener.
removeListener
Removes a listener. The listener will also be removed from all fields to which it is attached.
beginGroupPut
Begin a group of puts. This results in all registered PVListeners being called
endGroupPut
End a group of puts. This results in all registered PVListeners being called.
getTraceLevel
This can be used for debugging. There are currently three levels that are used by existing code.
0
Produce no trace messages.
1
Issue a message to std::cout whenever anything is created or destroyed.
2
In addition to lifetime messages also issue a message whenever the record is accessed by pvAccess client.
setTraceLevel
Set the trace level. Note that special, described below. provides a record support that allows a pvAccess client to set the trace level of a record.

The protected methods are:

PVRecord
The constructor. It requires a recordName and a top level PVStructure.
initPVRecord
This method must be called by derived class.
getPVStructure
Called by derived class.

class PVRecordField

class PVRecordField {
     public virtual epics::pvData::PostHandler,
     public std::tr1::enable_shared_from_this<PVRecordField>
public:
    POINTER_DEFINITIONS(PVRecordField);
    PVRecordField(
        epics::pvData::PVFieldPtr const & pvField,
        PVRecordStructurePtr const &parent,
        PVRecordPtr const & pvRecord);
    virtual ~PVRecordField();
    virtual void destroy();
    PVRecordStructurePtr getParent();
    epics::pvData::PVFieldPtr getPVField();
    std::string getFullFieldName();
    std::string getFullName();
    PVRecordPtr getPVRecord();
    bool addListener(PVListenerPtr const & pvListener);
    virtual void removeListener(PVListenerPtr const & pvListener);
    virtual void postPut();
protected:
    PVRecordFieldPtr getPtrSelf()
    {
        return shared_from_this();
    }
    virtual void init();
    virtual void postParent(PVRecordFieldPtr const & subField);
    virtual void postSubField();
private:
...
};

When PVRecord is created it creates a PVRecordField for every field in the PVStructure that holds the data. It has the following methods:

PVRecordField
The constructor.
~PVRecordField
The destructor.
destroy
Called by PVRecordStructure when it's destroy method is called.
getParent
Get the parent PVRecordStructure for this field.
getPVField
Get the PVField associated with this PVRecordField.
getFullFieldName
This gets the full name of the field, i.e. field,field,..
getFullName
This gets recordName plus the full name of the field, i.e. recordName.field,field,..
getPVRecord
Returns the PVRecord to which this field belongs.
addListener
Add A PVListener to this field. Whenever this field or any subfield if this field is modified the listener will be notified. PVListener is described below. Before a listener can call addListener it must first call PVRecord.registerListener.
removeListener
Remove a PVListener.
postPut
This is called by the code that implements the data interface. It is called whenever the put method is called.

class PVRecordStructure

class PVRecordStructure : public PVRecordField {
public:
    POINTER_DEFINITIONS(PVRecordStructure);
    PVRecordStructure(
        epics::pvData::PVStructurePtr const & pvStructure,
        PVRecordStructurePtr const & parent,
        PVRecordPtr const & pvRecord);
    virtual ~PVRecordStructure();
    virtual void destroy();
    PVRecordFieldPtrArrayPtr getPVRecordFields();
    epics::pvData::PVStructurePtr getPVStructure();
    virtual void removeListener(PVListenerPtr const & pvListener);
    virtual void postPut();
protected:
    virtual void init();
private:
...
};

When PVRecord is created it creates a PVRecordStructure for every structure field in the PVStructure that holds the data. It has the following methods:

PVRecordStructure
The constructor.
~PVRecordStructure
The destructor.
getPVRecordFields
Get the PVRecordField array for the subfields
getPVStructure
Get the PVStructure for this field.
removeListener
Remove a PVListener.
postPut
This is called by the code that implements the data interface. It is called whenever the put method is called.

class PVRecordClient

class PVRecordClient {
    POINTER_DEFINITIONS(PVRecordClient);
    virtual ~PVRecordClient();
    virtual void detach(PVRecordPtr const & pvRecord);
};

where

~PVRecordClient
The destructor.
detach
The record is being removed from the master database,

class PVListener

class PVListener {
    virtual public PVRecordClient
public:
    POINTER_DEFINITIONS(PVListener);
    virtual ~PVListener();
    virtual void dataPut(PVRecordFieldPtr const & pvRecordField) = 0;
    virtual void dataPut(
        PVRecordStructurePtr const &
        requested,PVRecordFieldPtr const & pvRecordField) = 0;
    virtual void beginGroupPut(PVRecordPtr const & pvRecord) = 0;
    virtual void endGroupPut(PVRecordPtr const & pvRecord) = 0;
    virtual void unlisten(PVRecordPtr const & pvRecord);
};

where

~PVListener
The destructor.
dataPut(PVRecordFieldPtr const & pvRecordField)
pvField has been modified. This is called if the listener has called PVRecordField::addListener for pvRecordField.
dataPut( PVRecordStructurePtr const & requested,PVRecordFieldPtr const & pvRecordField)
pvField has been modified. Requested is the field to which the requester issued a pvField-&addListener. This is called if the listener has called PVRecordField-&addListener for requested.
beginGroupPut
A related set of changes is being started.
endGroupPut
A related set of changes is done.
unlisten
The record is being destroyed. The listener must release all access to the record.

class PVDatabase

class PVDatabase : virtual public epics::pvData::Requester {
public:
    POINTER_DEFINITIONS(PVDatabase);
    static PVDatabasePtr getMaster();
    virtual ~PVDatabase();
    virtual void destroy();
    PVRecordPtr findRecord(std::string const& recordName);
    bool addRecord(PVRecordPtr const & record);
    epics::pvData::PVStringArrayPtr getRecordNames();
    bool removeRecord(PVRecordPtr const & record);
private:
    PVDatabase();
};

where

getMaster
Get the master database. This is the database that localChannelProvider access.
~PVDatabase
The destructor.
destroy
This is called by remote channelAccess when process exits. This destroys and removes all records in the PVDatabase.
findRecord
Find a record. An empty pointer is returned if the record is not in the database.
addRecord
Add a record to the database. If the record already exists it is not modified and false is returned.
getRecordNames
Returns an array of all the record names.
removeRecord
Remove a record from the database. If the record was not in the database false is returned.

pvAccess

This is code that provides an implementation of channelProvider as defined by pvAccess. It provides access to PVRecords and is accessed by the server side of remote pvAccess. It uses the copy and monitor facilities from pvDataCPP and connects them to a PVRecord.

The implementation is a complete implementation of channelProvider and channel except for channelRPC, which is implement by pvAccess as a separate channel provider.

The following provides a brief description of each channel method that is implemented.

channelProcessLocal

Needs to be described.

channelGetLocal

Needs to be described.

channelPutLocal

Needs to be described.

channelPutGetLocal

Needs to be described.

channelArrayLocal

Needs to be described.

MonitorLocal

This is the code that implements monitors on changes to fields of a PVRecord. Because it is called by pvAccess client (monitor methods) and by PVRecord (when postPut is called), it must be careful to prevent deadlocks. The implementation is via class MonitorLocal (implemented in monitorFactory.cpp) and PVCopyMonitor. MonitorLocal is the interface between pvAccess and PVCopyMonitor. PVCopyMonitor is the interface between MonitorLocal and PVRecord. MonitorLocal manages a MonitorElement queue. While monitoring is active (between start and stop) it keeps an active element for use by PVCopyMonitor. While monitoring is active PVCopyMonitor updates the active monitor element whenever a postPut is issued to any field being monitored.

The following two sections provide a few more details about MonitorLocal and PVCopyMonitor.

MonitorLocal

MonitorLocal implements the following abstract base classes:

Monitor
This is described by pvDataCPP. It has methods start, stop, poll, and release. These methods are called by the pvAccess client
PVCopyMonitorRequester
This has methods releaseActiveElement and unlisten. These methods are called by PVCopyMonitor.
MonitorLocal manages the following:
MonitorElementQueue
This is a queue of monitor elements. A Queue is implemented by pvDataCPP and used by MonitorLocal. It is a finite queue. A monitor element is described by pvDataCPP. It has fields pvStructure, changedBitSet, and overrunBitSet. The pvStructure holds data for a subset of the fields in a PVRecord. The changedBitSet and overrunBitSet describe changes between monitor event. MonitorLocal creates an instance of PVCopy (implemented by pvDataCPP), which manages the interaction between the set of fields being monitored and the fields in the top level PVStructure of the PVRecord. pvCopy is also used to create the pvStructure for each monitor element.
activeElement
Whenever monitoring is active monitorLocal keeps an active element for use by pvCopyMonitor. It changes the active element based on calls to poll (by the client) and calls to releaseActiveElement (by pvCopyMonitor). If there are no free element when releaseActiveElement is called the current active element is returned. If a free element is available the client is notified that a new monitor element is available and the free element becomes the active element.

A brief description on each method in MonitorLocal is:

start
Called by client. With a lock held it clears the monitorElement queue and allocates an active element. With no lock held calls pvCopyMonitor->startMonitoring(activeElement)
stop
Called by client. With no lock held calls pvCopyMonitor->stopMonitoring(activeElement)
poll
Called by client. With a lock held it calls queue->getUsed();
release
Called by client. With a lock held it calls queue->releaseUsed();
releaseActiveElement
Called by PVCopyMonitor with no locks held. With a lock held it tries to get a new free element. If it can't it just returns the current active element. Otherwise it does the following. Using the activeElement it updates the pvStructure and compresses the changed and overrun bitSet. It then calls queue->setUsed(activeElement); It then sets the active element to the new free element. With no lock held it calls monitorRequester->monitorEvent(getPtrSelf()) and finally returns the new active element,
unlisten
With no lock held it calls monitorRequester->unlisten(getPtrSelf());

PVCopyMonitor

pvCopyMonitor is the code that manages changes to fields in the record. It is called by PVRecord whenever a postPut is issued to a field. pvCopyMonitor uses the active monitor element provided by monitorFactory. Note that this method is called with the record locked. It only modifies the changedBitSet and overrunBitSet of the active element but never modifies the pvStructure.

A brief description of the pvCopyMonitor methods is:

startMonitoring
With no lock held it sets its monitorElement to the startElement passed by monitorLocal and calls pvRecord->addListener(getPtrSelf()). It locks the pvRecord. It calls calls addListener for every field in the record that is being monitored. It clears the overrun and changed bit sets. It sets bit 0 of the changed bit set and calls pvCopyMonitorRequester->releaseActiveElement(); Thus the client will get the initial values for every field being monitored. The record is unlocked and the method returns to the caller.
stopMonitoring
With no lock held it calls pvRecord->removeListener(getPtrSelf());
dataPut
This is called because of a call to postPut. It is called with the record locked. It updates the changed and overrun bitSets. It isGroupPut is false it calls pvCopyMonitorRequester->releaseActiveElement(). Otherwise it sets dataChanged true.
beginGroupPut
With a lock held it sets isGroupPut true and dataChanged false.
endGroupPut
With a lock held it sets isGroupPut false. With no lock held and dataChanged true it calls pvCopyMonitorRequester->releaseActiveElement()
unlisten
Just calls pvCopyMonitorRequester->unlisten();

special

This section provides two useful record support modules and one that is used for testing.

traceRecord

This implements a PVRecord that allows a client to set the trace level of a record. It follows the pattern of a channelPutGet record:

traceRecord
    structure argument
        string recordName 
        int level 0
    structure result
        string status 
where:
recordName
The name of the record to set the trace level.
level
The level to set. The meaning is:
0
No trace messages generated
1
Lifecycle messages will be generated. This all channel create and destroy instances will be shown.
2
In addition to lifecycle messages a message will be generated for each get and put request.
>2
Currently no definition
result
The result of a cannelPutGet request

testExampleServerMain.cpp has an example of how to create a traceRecord:

PVDatabasePtr master = PVDatabase::getMaster();
PVRecordPtr pvRecord;
String recordName;
bool result(false);
recordName = "traceRecordPGRPC";
pvRecord = TraceRecord::create(recordName);
result = master->addRecord(pvRecord);
if(!result) cout<< "record " << recordName << " not added" << endl;