NOTICE FOR FUTURE RELEASES: AVOID CHANGING THE NUMBERING! The tests refer to the sections, incl. links and unlinked references from tests or other parts of the specification. These break, or even worse become wrong, when they are not changed consistenty!
Introduction
This document describes the behaviour of the TransferElement base class, the NDRegisterAccessor base class, as well as requirements for all implementations (backends and decorators). The behaviour has been defined such that consistent behaviour with other libraries in the framework is ensured, namely ApplicationCore with the specifications for exception handling, initial value propagation and propagation of the data validity flag.
A. Definitions
- 1. A process variable is the logical entity which is accessed through the TransferElement. Outside this document, it is sometimes also called register.
- 1.1 A process variable can be read-only, write-only or read-write (bidirectional).
- 1.2 A process variable has a data type.
- 1.3 A process variable has a fixed number of elements and a number of channels.
- 1.3.1 For scalars, both the number of elements and channels are 1.
- 1.3.2 For 1D arrays, the number of channels is 1.
- 2. A device is the logical entity which owns the process variable. The device can be a piece of hardware, another application or even the current application.
- 2.1 The first two cases (piece of hardware and another application) are considered identical, since just different backends are used for the communication.
- 2.2 The third case (current application) is when using the ChimeraTK::ControlSystemAdapter::ProcessArray e.g. in ApplicationCore.
- 2.3 The application-side behavior of all three cases is identical. The requirements for the implementation are slightly different in some aspects. This will be mentioned where applicable.
- 3. A transfer is the exchange of data between the application and the device, using a transfer protocol which determines the technical implementation. The protocol used for the transfer is determined by the backend and hence all details about the protocol are abstracted and not visible by the application.
- 4. An operation is the action taken by the application to read or write data from or to a device. An operation is related to a transfer, yet it is to be distinguished. The transfer can e.g. be initiated by the device, while the operation is always initiated by the application.
- 5.The application buffer (sometimes also called user buffer outside this document) is referring to the buffer containing the data and meta data, which is accessible to the application.
- 5.1 It can be accessed through the following (non-virtual) functions:
- 5.2 If not stated otherwise, the term application buffer refers to all components of the buffer.
- 5.3 The content of the buffer is filled with data from the device in read operations, and transferred to the device in write operations.
- 5.4 The content of the buffer can always be modified by the application. (*)
- 5.5 NDRegisterAccessor::buffer_2D is referred to as data buffer, while TransferElement::_versionNumber and TransferElement::_dataValidity form the meta data buffer
- 6. Placeholders are used to summarise various function names:
- 6.1 xxxYyy(), public, operations called by the application (through the TransfeElementAbstractor)
- 6.2 preXxx(), public
- 6.3 doPreXxx(), protected virtual
- 6.4 xxxTransferYyy(), public
- 6.5 doXxxTransferYyy(), protected
- 6.6 postXxx(), public
- 6.7 doPostXxx(), protected virtual
- 7. Virtual and non-virtual functions
- 7.1 The non-virtual functions preXxx(), xxxTransferYyy(), and postXxx() implement common, decorating functionality like exception handling in the TransferElement base class
- 7.2 They internally call their virtual counterparts which start with 'do'
- 7.3 The virtual do-functions are the actual implementations of the transfer or pre/post action, which are specific for each backend, decorator etc.
- 8. Queues
- 8.1 The TransferElement::_readQueue is a cppext::future_queue of type 'void' which exists in each transfer element.
- 8.2 For transfer elements with AccessMode::wait_for_new_data it usually is a deferred continuation queue of another future_queue which is transporting the data, called implementation-specific data transport queue throughout this document. The data type of this queue is implementation dependent.
- 8.3 Common functionality like exception handling and waiting for new data is implemented on TransferElement::_readQueue.
(*) Comments
- 5.4 The buffer is accessed by the read/write operatons and must not be changed at this time. As transfer elements are not thread safe (B.1) this means the application will either perform an operation or otherwise change the buffer ad libitum.
- 6.5 doReadTransferSynchonously() is currently called doReadTransfer(). This should be renamed.
B. Behavioural specification
- 1. TransferElements are not thread safe
- 2. Data types of the application data buffer:
- 2.1 The following data types are supported for the application data buffer:
- 2.2 Applications select a data type for the buffer via the UserType template argument of Device::getZzzRegisterAccessor() resp. DeviceBackend::getRegisterAccessor().
- 2.3 If needed, the register value is converted between the actual register's data type and the application-selected UserType.
- 2.3.1 Reading from an actual void register into a UserType which is not ChimeraTK::Void fills the user buffer with 0. The user buffer has 1 channel with 1 element.
- 2.3.2 Writing from a user buffer with ChimeraTK::Void to an actual non-void value writes 0 to the register. The user buffer matches the register's dimension.
- 2.4 In read operations, values outside the possible range of the UserType are moved to the nearest possible value (rounding and clamping).
- 2.5 In write operations, values outside the possible range of the actual register's data type are moved to the nearest possible value (rounding and clamping).
- 2.5.1 If the transfer protocol supports data validity flags, the data is flagged with DataValidity::faulty in case of an overflow/underflow. (*)
- 2.6 All values can be converted into std::string, hence it is always possible to obtain an accessor with std::string as UserType.
- 2.7 After construction, the data buffer is initialised with default-constructed values (i.e. 0 resp. empty string).
- 3. Modes of transfers
- 3.1 Read operations:
- 3.1.1 The flag AccessMode::wait_for_new_data determines whether the transfer is initiated by the device side (flag is set) or not.
- 3.1.2 If AccessMode::wait_for_new_data is not set, read operations
- 3.1.2.1 obtain the current value of the process variable (if possible/applicable by synchronously communicating with the device) [U],
- 3.1.2.2 have no information whether the value has changed,
- 3.1.2.3 behave identical whether read(), readNonBlocking() or readLatest() is called,
- 3.1.2.4 readNonBlocking() and readLatest() always return true. [T]
- 3.1.3 If AccessMode::wait_for_new_data is set
- 3.1.3.1 read() blocks until new data has arrived, [T]
- 3.1.3.2 readNonBlocking() does not block and instead returns whether new data has arrived or not, [T]
- 3.1.4 readLatest() is merely a convenience function which calls readNonBlocking() until no more new data is available. [T]
- 3.2 Write operations
- 3.2.1 do not distingish on which end the transfer is initiated. The API allows for application-initiated transfers and is compatible with device-initiated transfers as well.
- 3.2.2 can optionally be "destructively", which allows the implementation to destroy content of the application buffer in the process. [U]
- 3.2.2.1 Applications can allow this optimisation by using writeDestructively() instead of write().
- 3.2.2.2 Applications are not allowed to use the content of the application buffer after writeDestructively().(*)
- 3.2.3 return whether previous data has been lost, as reported by writeTransfer()/writeTransferDestructively() (c.f. 7.2).
- 4. Stages of an operation initiated by calling the public high level functions xxxYyy() (see. A.6.1) [T (order of the stages)]
- 4.1 preXxx(): calls doPreXxx() of the implementation to allow preparatory work before the actual transfer. doPreXxx() can be empty if nothing is to be done (*) [T]
- 4.2 xxxTransferYyy():
- 4.2.1 readTransfer() [T]
- If wait_for_new_data is set, it waits until new data has been received and returns
- If wait_for_new_data is not set, it calls doReadTransferSynchrously()
- 4.2.2 readTransferNonBlocking() [T]
- If wait_for_new_data is set, it returns immediately with the information whether new data has been received
- If wait_for_new_data is not set, it calls doReadTransferSynchrously() and returns true
- 4.2.3 writeTransferYyy() calls the corresonding doWriteTransferYyy() [T]
- 4.2.4 Transfer implementations do not change the application buffer [U]
- 4.3 postXxx(): calls doPostXxx() of the implementation to allow follow-up work after the actual transfer. [T]
- 4.3.1 In read transfers, doPostRead() is the only place where the application buffer may be changed (*).
- 4.3.2 In write transfers, postWrite() updates the version number of the application buffer TransferElement::_versionNumber to the version number provided to the write() call, if no exception is (re-)thrown in doPostWrite() (cf. B.11.3). [T]
- 5. preXxx() and postXxx(), resp. doPreXxx() and doPostXxx(), are always called in pairs. (*)
- 5.1 This holds even if exceptions (both ChimeraTK::logic_error and ChimeraTK::runtime_error) are thrown (see 6). (*) [T]
- 5.2 The implementations of preXxx() and postXxx() ignore duplicate calls, such that a call to doPreXxx() is never followed by another call to doPreXxx() before doPostXxx() has been called, and vice versa. [T]
- 6. Exceptions thrown in doPreXxx(), doXxxTransferYyy() or received on the TransferElement::_readQueue (see A.8.1) are caught and delayed until postXxx() by the framework. This ensures that preXxx() and postXxx() are always called in pairs
- 6.1 If in preXxx() an exception is thrown, the corresponding xxxTransferYyy() is not called, instead directly postXxx() is called. [T]
- 6.2 To ensure 6.1 is guaranteed with decorators, the exception has to be caught and stored in the outermost decorator, i.e. in the public xxxYyy() which delegate to the transfers. [T]
- 6.3 To ensure that the ApplicationCore ExeptionHandlingDecorator works in combination with other decorators, each decorator has to pass the stored exception to its target, and it has to be re-thrown in the doPostRead() of the layer where the exception originated (see C.2)
- 6.4 When an exception is finally thrown by a TransferElement, the application buffer must still be unchanged. [U]
- 7. Return values of xxxTransferYyy():
- 7.1 readTransferNonBlocking() returns whether new data has been received (see 4.2.2)
- 7.2 writeTransfer() and writeTransferDestructively() return whether data has been lost. If it returns true, previous data was rejected in the process of the transfer. It is always guaranteed that the data of the current transfer is not lost. (*) [T, U]
- 7.3 For read operations, the return value is passed on to postRead() (via updateDataBuffer), to allow the doPostXxx() implementations to decide the right actions. In case of readTransfer(), updateDataBuffer is set to true. [T]
- 7.4 In case of an exception in either preRead() or readTransferYyy(), postRead() is called with updateDataBuffer = false because there was no successful transfer. (*) [T]
- 8. Read operations with AccessMode::wait_for_new_data:
- 8.1 Since the transfer is initiated by the device side in this case, the transfer is asynchronous to the read operation.
- 8.2 The backend fills any received values into the implementation-specific data transport queue, from which the readTransfer()/readTransferNonBlocking() operations will obtain the value (via the TransferElement::_readQueue continuation). [T, U]
- 8.2.1 If the queue is full, the last written value will be overwritten. [U (STILL INCOMPLETE)]
- 8.2.2 The backend may fill a ChimeraTK::detail::DiscardValueException to the queue, which has the same effect on the application side as if no entry was filled to the queue. (*) [T]
- 8.2.3 The continuation of the implementation-specific data transport queue stores the value so it is available in doPostRead(), where it is filled into the application buffer.
- 8.3 Runtime errors like broken connections are reported by the backend by pushing ChimeraTK::runtime_error exceptions into the queue. The exception will then be obtained by the read operation in place of a value. [U (first sentence), T (second sentence)]
- 8.4 The backend ensures consistency of the value with the device, even if data loss may occur on the transport layer. If necessary, a heartbeat mechanism is implemented to correct any inconsistencies at regular intervals. [U]
- 8.5 For transfer elements which are created before the device has successfully been opened and Device::activateAsyncRead() has been called (or after the device has seen an exception, see 9.) the backend makes sure that no data is filled into the implementation-specific data transport queue until the device has successfully been opened and Device::activateAsyncRead() is called (*). Also no runtime_errors are sent. We call this asyncronous read is not activated. [Broken/insensitive U].
- 8.5.1 When Device::activateAsyncRead() is called, the backend activates asyncronous read for all its transfer elements where AccessMode::wait_for_new_data is set (*) [U].
- 8.5.1.1 Each backend is responsible for activating asynchronous reads only on those TransferElements which it created. (cf. 9.2.1)
- 8.5.1.2 Meta-backends like the LogicalNameMappingBackend delegate Device::activateAsyncRead() to all of their target backends, as they also delegate the creation of the TransferElements to their targets.
- 8.5.2 In case the device does not send an initial value after the subscription, the accessor implementation must get an initial value synchronously and treat it as if it would have been received, i.e. push it to the queue. This must happen after the actual asynchronous sending has been turned on to make sure no update is missed. (*) [U]
- 8.5.3 If a transfer element is created while the device is opened, functional and Device::activateAsyncRead() has been called, the asynchronous read is activated immediately (incl. sending of the initial value). This ensures that either all or none of the transfer elements are receiving asyncronously send data. [Needs to be changed, most probably testing the wrong thing. U]
- 8.5.4 If Device::activateAsyncRead() is called while the device is not opened or has an error, this call has no effect. If it is called when no deactivated transfer element exists, this call also has no effect.
- 8.5.5 Device::activateAsyncRead() does not throw any exceptions.
- 8.5.6 When Device::activateAsyncRead() returns, it is not guaranteed that all initial values have been received already.
- 8.6 Blocking TransferElement::read() calls can be interrupted by the application via TransferElement::interrupt().
- 9. If one transfer element of a device has seen a ChimeraTK::runtime_error (*), all other transfer elements of the same device must also be aware of this. (*)
- 10. Removed. Became part of 9.
- 11. A VersionNumber object is attached to each data transfer.
- 11.1 VersionNumber objects
- 11.1.1 can be stricly ordered by their time of creation within the application (process lifetime) using the C++ comparison operators (<, ==, > etc.),
- 11.1.2 additionaly contain a std::chrono::system_clock timestamp which allows to weakly order the objects even across applications/processes,
- 11.1.3 are copyable, and
- 11.1.4 can be initiallised with a {nullptr} which yields a special instance that sorts before all normal instances. All {nullptr} constructed objects are equal (==).
- 11.1.5 The default constructor creates a new VersionNumber which is larger than all previously created VersionNumbers in a thread safe way.
- 11.2 VersionNumber objects are used
- 11.2.1 to determine which data is older (*), as needed e.g. by bidirectional process variable implementations [U], and
- 11.2.2 to build a consistent data set from multiple TransferElements (see DataConsistencyGroup). This requires the data sources (e.g. backend implementations) to attach the same VersionNumber to different TransferElements if the data is consistent. [U (STILL INCOMPLETE)]
- 11.2.3 From the previous two points it follows that backend implementations must create a new VersionNumber for all data that is received or read, or associcate a known version number according to 11.2.2
- 11.3 The VersionNumber of the last successfully written/read data of a TransferElement can be obtained through TransferElement::getVersionNumber() (cf. B.4.3.2). [T]
- 11.4 The VersionNumber for a transfer is given as an optional argument to TransferElement::write() resp. TransferElement::writeDestructively(). If the argument is missing, a new VersionNumber is generated.
- 11.5 If a transfer was not successful (exception has been thrown), the VersionNumber returned by TransferElement::getVersionNumber() does not change (cf. B.6.4). [T]
- 11.6 Before the first successful transfer, TransferElement::getVersionNumber() always returns the {nullptr} constructed VersionNumber. [T, U]
- 11.7 To be decided if we only allow 11.7.2: Bi-directional variables
- 12. TransferGroup and merging transfers:
- 12.1 TransferElements which are put into the same TransferGroup can optionally merge their data transfer to optimise for speed and to offer a best-effort data consistency.
- 12.1.1 TransferGroups do not enforce data consistency, since merging transfers is optional.
- 12.1.1.1 If a merge happens and consistency of the data can be guaranteed in a read operation, the VersionNumber of the corresponding TransferElements is set to the same value.
- 12.1.2 Structure of TransferElements in TransferGroups:
- 12.1.2.1 A TransferElement, which is directly added to the TransferGroup, is called high-level element.
- 12.1.2.2 Decorator-like implementations use one or more low-level elements which perform the actual register access. All low-level elements of one TransferElement are returned by TransferElement::getHardwareAccessingElements().
- 12.1.2.3 Low-level elements are not decorator-like, since they directly access the device.
- 12.1.2.4 There can be any number of decorator-like intermediate elements. The sum of all elements on all levels of one high-level element including the low-level elements but excluding the high-level element itself is called internal elements. All internal elements of one TransferElement are returned by TransferElement::getInternalElements().
- 12.1.3 After a TransferElement is added to a TransferGroup, TransferElement::replaceTransferElement() is called for each element in the group, passing each internal element in the group as a replacement. [T]
- 12.1.4 TransferElement::replaceTransferElement() uses the given replacement TransferElement for an optimisation, if possible. Depending on the protocol, this can mean:
- 12.1.4.1 An internal element is replaced 1:1 by the replacement, because it transfers the same data (cf. 12.1.5). This is a common case for decorator-like implementations.
- 12.1.4.1.1 A copy decorator must be placed around the replacement TrasferElement to avoid that the multiple users of the lower-level TransferElements are swapping out the data before it is seen by all users.
- 12.1.4.1.2 The use of a copy decorator is not required, if it is guaranteed that no such conflict may happen. This can only be the case if the replacement TransferElement is never used as a stand-alone TransferElement.
- 12.1.4.2 The given replacement can be extended such that the data of an internal element is included in the transfer; then the internal element can be replaced by the now-extended replacement.
- 12.1.4.3 If such replacement takes place, the behaviour visible to the application of the TransferElement does not change (exception: VersionNumbers in read opertions can reveal now-consistent data, see 12.1.1).
- 12.1.4.4 If no such optimisation is possible, the function simply does nothing.
- 12.1.5 TransferElement::mayReplaceOther() returns true, if the given TransferElement behaves identical to the one it is called on in every aspect, i.e. it accesses the same data on the same device, has the same UserType and the same AccessModeFlags, and (if applicable) uses the same data transformations (like fixed point conversion). It returns false otherwise. This is used by implementations of 12.1.4.1.
- 12.2 Adding TransferElements which cannot merge their transfers to the same TransferGroup is always allowed.
- 12.3 If TransferElements have been added to a TransferGroup, individual read/write operations of the TransferElements are no longer allowed and will throw a ChimeraTK::logic_error. Read/write operations are then only possible through the TransferGroup. [T]
- 12.4 A TransferGroup is becomes read-only (cf. TransferGroup::isReadOnly()), if at least one of the member TransferElements is read-only. [T]
- 12.5 Adding the same TransferElement twice is not allowed (logic_error). [T]
- 12.6 Adding one TransferElement to two different TransferGroups is not allowed (logic_error). [T]
- 12.7 Adding TransferElements with AccessMode::wait_for_new_data is not allowed (logic_error). [T]
- 12.8 Adding a copy of the a TransferElement which is already in the group (returned by two different calls to Device::getZzzRegisterAccessor() resp. DeviceBackend::getRegisterAccessor() with the same arguments) will replace one of the identical TransferElement with a decorator which copies the data from the other. [T]
- 12.9 Operations executed via TransferGroup::read() and TransferGroup::write(): [T (with sub-points)]
- 12.9.1 first call preXxx() on all high-level elements (cf. 12.1.2.1),
- 12.9.2 next call the corresponding xxxTransferYyy() on all low-level elements (cf. 12.1.2.2), and
- 12.9.3 finally call postXxx() on all high-level elements.
- 12.10 The TransferGroup implements a similar exception handling in TransferGroup::read() and TransferGroup::write() as their TransferElement counterparts. This especially means:
- 12.10.1 Before preXxx() is called, the TransferGroup checks all pre-conditions (device opened and operation allowed) to make sure no ChimeraTK::logic_error can occur.
- 12.10.1.1 If any ChimeraTK::runtime_error occurs while checking the pre-conditions, it is immediately passed on to the application. [T]
- 12.10.1.2 Any pre-condition which is not met will immediately result in a thrown ChimeraTK::logic_error [T]
- 12.10.1.3 The exceptions from 12.10.1.1 and 12.10.1.2 are not prioritised by type. The first detected error throws.
- 12.10.2 If ChimeraTK::runtime_error exceptions occur during preXxx()
- 12.10.2.1 still all preXxx() are executed.
- 12.10.2.2 the whole transfer phase is skipped.
- 12.10.2.3 This implies that TransferElements in TransferGroups must expect that the transfer is not executed, even if preXxx() has not thrown an exception.
- 12.10.3 ChimeraTK::runtime_error exceptions thrown by the low-level elements are propagated to the corresponding high-level elements so they are seen by their postXxx() functions (c.f. 6.3) [INCOMPLETE T]
- 12.10.4 postXxx() is called for all high level elements, even if some postXxx() throw exceptions (c.f. 5). [T]
- 12.10.5 The first exception that has been caught in 12.9.1, 12.9.2 or 12.9.3 is re-thrown. [INCOMPLETE T]
- 12.11 If a ChimeraTK::runtime_error is thrown, the content of the application buffers of all elements is not changed. (c.f. 6.4)
- 12.12 The TransferGroup calls postRead() on copy decorators first (see 12.1.4.1.1 and 12.8).
- 13. ReadAnyGroup [TODO]
- 14. DataConsistencyGroup [TODO]
- 15. AccessModeFlags
- 16. This section will be moved from C.2
(*) Comments
- 2.5.1 Backends which don't support data validity flags (like the PCIe backend) just get the clampled value. The DOOCS backend however, which supports data validity, sets the outgoing data to invalid in addition.
- 3.2.2.2 The optimisation is still optional, backends are allowed to not make use of it. In this case, the content of the application buffer will be intact after writeDestructively(). Applications still are not allowed to use the content of the application buffer after writeDestructively().
- 4.1 preXxx() is part of the operation, not of the actual transfer. In case of reads with AccessMode::wait_for_new_data the transfer is asynchronously initiated by the device and not connected to the operation. Hence backend implementations usually have an empty doPreWrite(), but decorator-like implementations still can use it to execute preparatory tasks.
- 4.3.1 In write operations the buffer might be swapped out in doPreWrite() and swapped back in doPostWrite() to restore it for non-destructive write operations to avoid copying of large arrays.
- 5. Reason: It might be that the user buffer has to be swapped out during the transfer (while taking away the ownership of the calling code), and this must be restored in the postXxx action.
- 5.1 The boost::thread_interrupted exception, which is thrown internally as described in 8.6, is treated equally.
- 7.2 Usually, writes are implemented as synchronous transfers, in which case no previous data can be lost. In case of asynchronous write transfers (as e.g. implemented in the ControlSystemAdapter's ProcessArray), the implementation must ensure the specified behaviour e.g. by using cppext::future_queue::push_overwrite() or a similar functionality. Please keep in mind that the return value of cppext::future_queue::push_overwrite() does not guarantee which data is lost only if concurrent push_overwrite() calls are executed in a multi-producer environment. TransferElements are not thread safe anyway, hence push_overwrite() will always overwrite old data in this context.
- 7.4 This is required by the ApplicationCore::ExceptionHandlingDecorator. It suppresses the exception, but decorators around it must not change their data buffer (c.f. E.6.1.1).
- 8.2.2 This allows to discard values inside a continuation of a cppext::future_queue. It is used e.g. by the ControlSystemAdapter's BidirectionalProcessArray. [TBD: It could be replaced by a feature of the cppext::future_queue allowing to reject values in continuations...]
- 8.5 Device::open() does not automatically activate the asyncronous sending because the device might need some initalisation setting to produce valid data (for example setting the correct ADC range). By delaing the activation of asyncronous reads the application has the possibility to do the initialisation before the first data is being send, and can avoid invalid initial values on the process variables.
- 8.5.1 Conceptually activating an asyncronous read is like subscribing a variable, and deactivating it is like unsubscribing a variable in a publish-subscribe pattern. The actual implementation depends on the details of the protocols.
- 8.5.2 As the asynchonous mechanism and a synchronous read are two idependent channels there are potential race condition, depending on the exact protocol. The backend has to avoid this if possible. If it cannot be avoided, the implementation must make sure that the last value in the queue is the newest value, and this is not dopped or missed, even if the values before are not in order or send twice.
- 8.6.6 The current default implementation in the TransferElement base class is always throwing a ChimeraTK::logic_error to inform the programmer that the function has to be overridden for all implementations with wait_for_new_data.
- 8.6.6.1 This requirement is merely a work-around for not knowing the type of the implementation-specific data transport queue in the TransferElement/NDRegisterAccessor base classes. It is planned to change this, in which case interrupt() may be implemented in one of the base classes. Hence, no additional, backend-specific code may be placed in the implementation now.
- 8.6.6.2 This is already implemented in the NDRegisterAccessorDecorator base class.
- 9. The ChimeraTK::runtime_error is the only exception that conceptually is recoverable. This is done by calling DeviceBackend::open(). Other exceptions are not recoverable by the running application:
- ChimeraTK::logic_error is a programming or configuration mistake.
- The boost::thread_interrupted is thrown when TransferElement::interrupt() is called. It means that a blocking read() has been interrupted on request. It is not an error and hence there is nothing to recover.
- 9. It does not matter if the exception occured in an asynchronous or synchronous read, or in a write operation.
- 9.1 It depends on the implementation whether the backend already has done 9.3 and 9.4 when the transfer elements first sees the exeption and then reports it back again via DeviceBackend::setException(), or if it only happend in that function. The important part is that meta-backends and the user application can trigger this situation (see 9.2.1 and 9.2.3)
- 9.1.4 This functionality is already implemented in the NDRegisterAccessorDecorator base class. It does not apply for decorator-like TransferElements which are created by meta-backends. They have to set their own and their target's _exceptionBackend to the meta-backend (c.f. 9.1.2).
- 9.2 'setException() triggers the actions' does not mean all those actions are completed. Especially it is not guaranteed that all async transfers are already done and the exceptions are already send when the setException() call returns.
- 9.2.3 For instance a watchdog which monitors a reference register to detect firmware reboots that is not seen on the transport layer can trigger the exception state to inhibit asynchronous transfers while running a recovery procedure.
- 9.3.1 Open can be called again on an already opened backend to start error recovery.
- 9.3.2 If an asynchronous read transfer is the first one to detect the exception, the implementation must make sure that it is only pushed once into the queue, and informing "all" transfer elememnts with wait_for_new data does not send it again if it was already put into the queue.
- 9.3.4 Avoid race conditions here. The call to Device::activateAsyncRead() usually is done from a different thread than the transfer which caused the exception.
- 11.2.1 This means the version number has to be created as soon as data is received, for instance in the receiver thread of a TransferElement with AccessMode::wait_for_new_data, and not in the doPostRead() of the read() operation.
- 11.4.1 Smaller version numbers on later write calls of a different instance can conceptionally not be avoided if the instances are used by different, unsynchronised threads. Even within the same thread, the information written to different instances can come from different sources and hence might have independent version numbers.
- 11.7.1 If a write call is done there might be older data in the read queue, which results in a lower version number once a read is called.
- 11.7.2 The BiDirectionalProcessArray is implemented like this.
- 12.8.2 The reason is that there needs to be some entity which holds the TransferElement to be replaced. If it is directly used by the calling code, the TransferGroup has no means to replace it.
C. Requirements for all implementations (full and decorator-like)
- 1. Other exceptions than ChimeraTK::logic_error and ChimeraTK::runtime_error are not allowed to be thrown or passed through at any place under any circumstance (unless of course they are guaranteed to be caught before they become visible to the application, like the detail::DiscardValueException). The framework (in particular ApplicationCore) may use "uncatchable" exceptions in some places to force the termination of the application. Backend implementations etc. may not do this, since it would lead to uncontrollable behaviour.
2. This section wil be moved to B.16.
In doPostXxx no new ChimeraTK::runtime_error or ChimeraTK::logic_error are thrown. Exceptions that were risen in doPreXxx or doXxxTransferYyy, or that were received from the queue (see B.8.3) are rethrown. (*)
- 2.1 The TransferElement base class stores the caught exceptions in TransferElement::_activeException. [T]
- 2.2 postXxx() is re-throwing the exception after delegating to doPostXxx(). [T]
- 2.3 Decorators delegate the TransferElement::_activeException to their target using TransferElement::setActiveException(). If the decorator's _activeException is not nullptr, the targets _activeException is replaced by it, and the decorator's _activeException, which is handed by reference, is set to nullptr (*). The target is now responsible for handling the exception. If the decorator's _activeException already is nullptr, setActiveException() has no effect, so an active exception in the target is not overwritten (*). (*) [T (only setActiveException())]
- 2.3.1 Decorators which are throwing themselves in doPreXxx(), before delegating to the target preXxx(), remember this. If they did throw in doPreXxx(), then in doPostXxx() they do not call setActiveException() of the target and do not delegate to postXxx(). The exception is then re-thrown by the decorator's postXxx().
- 3. ChimeraTK::runtime_error in principle are recoverable. It is thrown, if the device (including the communication link) does not behave as expected. A later call to the same function (after the recovery has been triggered, cf. B.9.3.1 resp. B.9.4.1) must be able to succeed (for instance if a network outage has been resolved). ChimeraTK::runtime_error may only be thrown in
- 4. (removed)
- 5. ChimeraTK::logic_error must follow strict conditions. It is thrown, if the application (including its configuration files) does not behave as expected.
- 5.1 logic_errors must be deterministic. They must always be avoidable by calling the corresponding test functions before executing a potentially failing action (*), and must occur if the logical condition is not fulfilled and the function is called anyway.
- 5.2 Any logic_error must be thrown as early as possible (*). They are thrown if and only if one of the following conditions are met:
- 5.2.1 A register does not exists my that name
- 5.2.1.1 Can be checked in the catalogue (*)
- 5.2.1.2 Thrown in the constructor. [U]
- 5.2.2 The size or dimension of the requested TransferElement is too large (*)
- 5.2.2.1 Can be checked in the catalogue.
- 5.2.2.2 Thrown in the constructor. [U]
- 5.2.3 The wrong AccessMode flags are provided
- 5.2.3.1 Can be checked in the catalogue.
- 5.2.3.2 Thrown in the constructor. [U]
- 5.2.4 (removed)
- 5.2.5 A read/write operation is started while the backend is still closed
- 5.2.6 A read operation is executed on a transfer element that cannot be read
- 5.2.7 A write operation is executed on a transfer element that cannot be written
- 5.2.8 A ScalarRegisterAccessor, OneDRegisterAccessor or TwoDRegisterAccessor is instantiated with UserType ChimeraTK::Void. (*)
- 5.2.9 A read-only VoidRegisterAccessor that does not have the wait_for_new_data flag is instantiated.
- 5.3 The information mentioned in 5.2 which determines whether a ChimeraTK::logic_error is thrown is considered to be constant for each TransferElement. Only when a ChimeraTK::runtime_error is thrown by the TransferElement, the information might change and applications are expected to check it again. [U]
- 5.3.1 If a device is changing this information on its own (e.g. makes a register read-only which was readable before), and the application hence performes an operation which is no longer possible, a ChimeraTK::runtime_error must be thrown (unexpected behaviour of the device). (*) [U]
- 5.3.2 If an implementation needs to obtain the information from the device (and hence the information might potentially change), it caches the information for each TransferElement separately. [U]
- 5.3.3 Implementations of the test functions always return the cached information, if available. [U]
- 5.3.4 If the test function needs to obtain the information from the device, a runtime_error might occur.
- 5.4 Remarks about how to follow these rules:
- 5.4.1 If there is an error in the map file and the backend does not want to fail on this in backend creation, the register must be hidden from the catalogue.
- 5.4.2 Implementations of doPreXxx() use these test functions to check the preconditions, but discard any runtime_error exception. The following doXxxTransferYyy() will then either throw a runtime_error (cf. 5.3.1) or succeed.
(*) Comments
- 2. This concerns all exceptions that were caught, not only ChimeraTK::runtime_error and ChimeraTK::logic_error.
- 2.3 It is important that after calling the target's setActiveException(), the decorator has a nullptr in its _activeException. This is for instance required if the target is the ApplicationCore::ExceptionHandlingDecorator. It suppresses the exception and then the outer layers must not throw any more.
- 2.3 Notice that if both the decorator and the target TransferElement have an active exception, the exception in the target is replaced. This is intentional. It can only happen if the same low level TransferElement is used by several high level elements in a TransferGroup. In this case the tranfer had not been executed because the high level elements had already seen the exception in preXxx, and the exception in the target can only be the exception which had been put in by another high level element in its doPostXxx, and thus has already been thrown.
- 2.3 This behaviour is implemented in doPostXxx() of the NDRegisterAccessorDecorator base class, which is usually called by decorator implementations.
- 3.3 This is for instance required by the ApplicationCore ExceptionHandlingDecorator, but also can in general be required in a multi-threaded scenario where the transfers happen in a different thread than the device recovery.
- 5.1 Test functions do not yet exist for everything. This needs to be changed!
- 5.2 Especially no logic_error must be thrown in doXxxTransferYyy() or doPostXxx(). All tests for logical consistency must be done in doPreXxx() latest.
- 5.2.1.1 It is legal to provide "hidden" registers not present in the catalogue, but a register listed in the catalogue must always work.
- 5.2.2 This also includes that the offset in a one dimensional case is so large that there are not enough elements left to provide the requested data.
- 5.2.5.2 The generic tests if a backend is opened, or if an accessor readable or writeable are intentionally not implemented in TransferElement because they would invovle additional virtual function calls. To avoid these each implementation has to implement the checks in doPreXxx().
- 5.2.8 A VoidRegisterAccessor has to be used instead.
- 5.3.1 To recover from the ChimeraTK::runtime_error, the application must call open(). At that point, the information is allowed to be changed. If the application fails to recheck the information, a retry of the failed operation will result in a ChimeraTK::logic_error. This behaviour follows the requirement to throw logic_errors only in doPreXxx(): if during a transfer the backend discovers that the operation is no longer allowed, a runtime_error must be thrown.
D. Requirements for full implementations (e.g. in backends)
- 1. If AccessMode::wait_for_new_data is specified:
- 1.1 As the implementation-specific data transport queue has to work with push_overwrite and push_overwrite_exception, it has to have a minimum length of 2.
- 1.2 TransferElement::_readQueue is initialised in the constructor. [TBD: Allow setting the queue length through the public API?]
- 1.3 implementation-specific data transport queue is pushed whenever new data has arrived. If important for the implementation, the return value of cppext::future_queue::push_overwrite() will tell whether data has been discarded (*).
- 1.4 In case an exception is detected during an asychronous transfer (for instance in a separate thread), the exception must be pushed to the implementation-specific data transport queue so it can be received through the TransferElement::_readQueue (*). The TransferElement implementation will then make sure that the exception is properly rethrown in postRead (just like for synchronous transfers).
- 2. In doPostRead() the whole application buffer (data buffer and meta data) must be updated together if updateDataBuffer is true and there was no exception, or not at all in all other cases.
(*) Comments
- 1.3 Either the currently pushed data or older data on the queue might be discarded. In any case there will be one fewer read operation because the number of entries in the queue could not be increased because it was full.
- 1.4 All data and exceptions must be pushed into the data transport queue. As TransferElement::_readQueue is a deferred continuation queue, a pop_wait() is waiting for data on the original queue and does not see that something has been but into the deferred queue.
E. Requirements for decorator-like implementations
- 1. If AccessMode::wait_for_new_data is specified, TransferElement::_readQueue is initialised in the constructor with a copy of the readQueue of the target TransferElement.
- 1.1 Decorator-like implementations with multiple targets must provide a readQueue e.g. by using cppext::future_queue::when_any() or cppext::future_queue::when_all().
- 2. All functions doPreXxx, doXxxTransferYyy and doPostXxx must delegate to their non-do counterparts (preXxx, xxxTransferYyy and postXxx). Never delegate to the do... of the target implementation functions directly.
- 3. If a function of the same instance should be called, e.g. if doWriteTransferDestructively() should redirect to doWriteTransfer(), or if doPostRead() should call doPostRead() of a base class, call to do-version of the function. This is merely to avoid code duplication, hence the surrounding logic of the non-do function is not wanted here.
- 4. Decorators must merely delegate doXxxTransferYyy, never add any functionalty there. Reason: TransferGroup might effectively bypass the decorator implementation of these functions.
- 5. All real decorators are in fact decorators of NDRegisterAccessors<USER_TYPE>. Each decoration level contains one NDRegisterAccessors<USER_TYPE>::buffer_2D. The decorator implementation must make sure that its buffer is correctly synchronised with the target's buffer.(*)
- 6. TransferElement::_versionNumber and TransferElement::_dataValitiy are also part of the applicaton buffer. They also must be synchronised.
- 6.1 When reading, TransferElement::_versionNumber and TransferElement::_dataValidity have to be copied over in doPostRead(), after a delegating to the target postRead() and only if there has been no exception.
- 6.1.1 Decorators then must always copy the meta data from their target, even if updateDataBuffer is false and NDRegisterAccessors<USER_TYPE>::buffer_2D is not updated. (*)
- 6.1.2 The data buffer is updated if there has been no exception and if updateDataBuffer is true.
- 6.2 When writing, TransferElement::_dataValidity has to be copied to the target in doPreWrite() before delegating. Do not manually synchonise the version number. This is done by the TransferElement base class in postWrite according to B.4.3.2.
(*) Comments
- 5. The NDRegisterAccessorDecorator base class already contains an implementation which does this in doPreXxx() and doPostXxx(). You usually call it from the Decorators implementation, as mentioned in 3.
- 6.1.1 This is required for by the ApplicationCore::ExceptionHandlingDecorator, which suppresses exceptions, but requires the meta data to be updated without changing the data buffer.