Tree Manager

Purpose

Offer a generic mechanism to present a device's data as a set of variables, organized in a hierarchical tree, which can be read, written, and monitored for changes by user applications.

The architecture consists of 3 parts:

Sub-modules

The tree manager is organized in several sub-modules:

Conceptz

The API works with paths (sequences of identifiers separated by dots), which denote nodes in trees. The node can be leaf nodes or non-leaf nodes. The non-leaf nodes have children, but carry no data to read or write. Leaf nodes carry some data to read/write/monitor, but have no children nodes.

In the example below, a and a.b are non-leaf nodes, with children {"a.b", "a.d"} and {"a.b.c"} respectively. Leaf nodes "a.b.c" and "a.d" have no children, but carry a value each.

-a
  +-b
  | +-c = 123
  +-d = 234

There are two kinds of trees involved in the tree manager : the logical tree, which is the one visible to users and servers, and the handler trees. There are many handlers, with different implementations of the get/set/notify services, and these handler trees can be mounted on the logical tree to actually implement it. For instance, if a handler's root is mounted on logical node "a.b", then the logical path "a.b.c.d" is associated with the handler path "c.d". One can also mount non-root nodes of a handler, for instance mount handler node "x.y" on logical node "a.b". In that case, logical path "a.b.c.d" is mapped with handler path "x.y.c.d". Leaf nodes can be mounted as well as non-leaf nodes. It is the tree manager's job to maintain all mappings between the handler trees and the logical tree: when implementing a handler, one never has to ever think of logical paths.

Here is an example of mapping between a logical tree and two handlers:

Logical tree:

+system
| +-position
| | +-latitude
| | +-longitude
| | +-elevation
| +-time
+-config
       +-server
       +-agent
       +-modem
       +-...

Handler trees:

aleos
  +-GPS_LATITUDE
  +-GPS_LONGITUDE
  +-GPS_ELEVATION
  +-TIME

config
  +-server
  +-agent
  +-modem
  +-...

In the example above, we want to map:

Each aleos variable above is mapped individually, but the whole config tree is mapped recursively: by mapping config:<root>=config, one gets for instance config:server.url=config.server.url, config:mediation.pollingperiod.GPRS=config.mediation.pollingperiod.GPRS, etc.

Naming conventions

In this module, the following variable naming conventions are chosen:

Handlers

Handlers work with paths relative to themselves; handler paths are not shown directly to user applications, they need to be mapped into the logical tree first.

The features needed from handlers are provided through methods; that is, every Lua object providing the get/set/register/unregister methods below is considered as a valid handler:

Logical tree

This is the API accessible to user applications. It offers get / set / notification services, on variables organized according to a map which is independent from the organization by handlers or within handlers.

Applications can read values with get(), write values with set(), register hook functions to be triggered everytime a variable changes with register():

Mapping and handler loading

A treemgr configuration consists of handlers and a mapping. Handlers are Lua objects which implement the handler:get(), handler:set(), handler:register() and handler:unregister() methods. The mapping is a set of bidirectional correspondances between logical tree nodes and handler nodes.

The mapping is stored in a CDB (Constant DataBase): it ensures conversions between the user view (logical paths) and the implementation view (handler path) in constant memory and time. It is built from "*.map" files, but once the DB is built, map files are not needed anymore.

The handlers are loaded lazily, when they are needed to fulfill a user request. They are identified by the name of the Lua module which implements them: if a handler is called 'agent.treemgr.handlers.ramstore', then a call to require 'agent.treemgr.handlers.ramstore' must return the handler object. By sitting directly above Lua's module management system, the handler loading system benefits from its flexibility and its various predefined loaders.

To facilitate build and deployment, treemgr is able to recompile its CDB from map files on target, if they are present and more recent than the DB. By convention, all the map files in persist/treemgr are compiled into the DB. Each map file describes the mappings of one handler.

A given treemgr configuration is described through a set of "*.map" files: each map file lists one handler, and a list of mappings, between nodes in this handler and nodes on the global, logical tree. The link between the handler's map and its code is maintained through Lua's require module system: the handler's name must be a valid Lua module name, and the result of loading this module must be the handler object, ready to run.

Map files are precompiled into CDB databases, for faster access in constant memory. CDB results could be cached in RAM if necessary, although it is not currently implemented.

Each architecture might have different ways to provide the same service, and might not provide the exact same set of services as others, depending e.g. on available hardware. By assembling the correct set of specific handlers, together with the map files which put variables at a standard place in the logical tree, one builds the target-specific implementation of the portable treemgr interface.

Implementation

One strong implementation constraint is that the logical tree can be big, and must not be required to fit entirely in RAM. It is therefore built as a read-only database, based on cdb.

There are four dictionaries to be kept in the database:

get

The get operation on leaf node is straightforward: llpath is translated to handler_id, hlpath, then the handler is retrieved and its get method is called with the proper argument.

A get on a non-leaf node must return the list of every direct child of the node. If the node is under a handler, then this handler's get method is in charge of providing this list. Hence, the get of a non-leaf node depends on the handlers mounted above it ("above" being understood inclusively, i.e. a node is considered to be above itself. For instance, if there's a non-leaf mountpoint on lpath "a.b", then a get request on "a.b" depends on this mountpoint, not on any mountpoint on "a" nor on the root node).

However, it also depends on handlers mounted below it. For instance, if there's a handler mounted on path "a.b.c", and the application gets the list of "a"'s children, then "b" must be included in this list, whether there's also a handler mounted on "a" or not.

If a get operation covers several paths mapping several handlers, a logical get can trigger several handler get operations. However, it makes sure to perform at most one get operation per handler, thus giving the handler an opportunity to optimize retrieval operations.

set

Set operations are mostly the same as leaf-node get requests. The differing part (writing hpaths rather than reading them) is specific to each handler.

register

Hook registrations call the register method(s) of the corresponding handler(s), so that they will know they must provide notifications. Those notifications are produced by calling notify, which will:

The logical registration on a non-leaf node must translate into registrations:

unregister

The application can unregister from logical paths it's not interested in anymore. Before unregistering from the corresponding hpath s, though, one must first ensure that no other hook needs this hpath. This requires to check whether the hpath is mapped to other lpaths, as it can be mapped more than once, either directly or through an ancestor node.