WildCAT: a framework for context-aware applications

  1. Introduction
  2. The data model
  3. User-level APIs
  4. Configuration
  5. Extension: writing adapters and providers

Introduction

WildCAT is a Java toolkit/framework which can be used to build context-aware applications. From the client applications point of view, it provides a simple and dynamic data model to represent the execution context of the application, and offers a simple API for the programmers to access this information both synchronously and synchronously (pull and push). Internally, it is a framework designed to facilitate the aggregation of contextual data by integrating existing monitoring technologies into a single common model.

Numerous monitoring solutions already exist in many domains (network and grid, performance, application-level, environment, etc.). Each represent real expertise in a specialized domain, but these solutions can not be easily combined to provide an overall picture, as they are generally ad hoc and heterogeneous. Building context-aware applications and autonomic control loops requires a systemic view of the target application and its environment, which covers domains currently targeted by different solutions. In order to build such a systemic view without re-implementing already existing solutions, one must be able to integrate to (called "providers" from now on) in a uniform way. In a sense, what is needed is a monitoring middleware, able to provide a uniform -- and hopefuly simple -- interface to a set of heterogeneous monitoring solutions.

WildCAT provides such a middleware platform, and is built around the following requirements:

WildCAT is designed to take different kinds of users into account, each of which has particular needs which influence the system's design:

The data model

WildCAT represents the execution context using a purposedly simple data model consisting in a tree of resources, each described by a set of attributes. The top-level, root resource is anonymous and can contain only sub-resources (no attributes). Every other resource is identified by a name, unique among all the sub-resources of a given parent resource, and can contain sub-resources and attributes. Attributes are named values attached to a resource they describe, and as for resources their names is unique among siblings. However, resources and attributes names are in different namespaces, so it is possible for a resource to have both a sub-resource and an attribute with the same name.

This model does not impose any particular semantics on the structure of resources and attributes. Depending on how the model is used, "r2 is a sub-resource of r1" can mean "r2 depends on r1", "r2 is a part of r1", "r2 is an aspect of r1", etc. WildCAT only provides a base mechanism, and does not impose a particular policy on how it should be used. For example, most applications map attributes to properties of the corresponding resource. This works if properties are atomic, but if one wants a more sophisticated modeling of properties (including for example the unit of measure, its precision, etc.), one would model properties as resources with different attributes for different aspects of the properties.

WildCAT uses a syntax inspired by Unix file names to denote elements in the context while being indenpendent on the actual implementation. A path can denote either a resource, an attribute, or a set of resources or attributes matching a particular pattern.

      /sys/storage/memory              // A specific resource
      /sys/storage/disks/hda#removable // A specific attribute
      /sys/devices/input/*             // All the (direct) sub-resources of input
      /sys/devices/input/mouse#*       // All the attributes of mouse
      /cluster/nodes/*/load#cpu        // The processing loads of all the nodes in a cluster
    

Note that a path can be syntactically valid but denote a location which does not exist.

Of course, since the context being modeled is highly dynamic, so is WildCAT's data model. Each change occuring in the context model is represented by an event, which can be observed (other kinds of events can also be observed, but they do not strictly correspond to primitive changes in the model). The possible kinds of events are:

User-level APIs

The user-level API, which is used by the client programmer, is designed to be simple and easy to learn. All the interactions of the programmer with WildCAT happen through an instance of the Context class (multiple instances can be created inside a single application).

The first set of methods implement synhronous requests (pull mode) and allow clients to discover the structure of the context and to request the immediate value of an attribute:

      Collection<Path> lookup(Path query) { ... }
      Collection<Path> lookup(String query) { ... }
      Object lookupAttribute(Path attr) { ... }
      Object lookupAttribute(String attr) { ... }
    

The main methods are lookup(Path) and lookupAttribute(Path), but both provide variants taking strings instead of pre-parsed Paths for convenience.

lookup(Path) takes an absolute path as a parameter, which can be either definite (i.e. representing a specific location like /geo/location#latitude) or a pattern (i.e. representing of set of potential locations, like /sys/devices/*). It returns the paths of all the elements currently in the context (resources and attributes) which match the query. This method is very generic and covers several use cases:

      // Testing whether an element exists in the context
      boolean exists = !context.lookup("/sys/devices/keyboard").isEmpty();

      // Finding all the sub-resources
      Collection<Path> devices = context.lookup("/sys/devices/*");

      // Finding all the attributes of a resource
      Collection<Path> mouseAttrs = context.lookup("/sys/devices/mouse#*");

      // Finding a specific attribute on a set of resources
      Collection<Path> vendors = context.lookup("/sys/devices/*#vendor");

      // Finding all the siblings of a resource
      // See the Path interface for other methods to manipulate paths
      Collection<Path> siblings = context.lookup(aPath.getParent().append("*"));
    

The second method, lookupAttribute(Path) takes an absolute path (either as a string or a real Path object) which must designate a definite attribute (i.e. not a pattern), and returns the current value of the attribute in question, or null if it does not exist.

      Double load = (Double) context.lookupAttribute("/sys/cpu/load#load_1min");
    

The second set of methods provided by Context enable a client to be notified asynchronously when some events of interest occur (push mode). These methods follow the publish/subscribe pattern: the client first registers its interest in a kind of event, and from this point on WildCAT will send it asynchronous notifications each time a corresponding event occurs. The methods in Context corresponding to this interface are:

    Object register(EnumSet<EventKind> evtKinds, Path path, ContextListener listener) { ... }
    Object registerExpression(EnumSet<EventKind> evtKinds, String expression, ContextListener listener) { ... }
    void unregister(Object cookie) { ... }
    

The first method, register() takes a "bitmask" representing the kinds of events the client is interested in (using the EventKind enumeration), the path on which the events must be detected (this path can be a pattern), and finally a listener object (which will receive the event notifications). For exemple, to get notified each time a new input device is plugged or unplugged:

      context.register(EnumSet.of(RESOURCE_ADDED, RESOURCE_REMOVED),
                       Context.createPath("/sys/devices/input/*")), myListener);
    

Where myListener implements the ContextListener interface:

      public interface ContextListener {
          void attributeAdded(Path attr, long timeStamp);
          void attributeRemoved(Path attr, long timeStamp);
          void attributeChanged(Path attr, Object oldValue, Object newValue, long timeStamp);
          void resourceAdded(Path res, long timeStamp);
          void resourceRemoved(Path res, long timeStamp);
          void expressionValueChanged(Object cookie, Object oldValue, Object newValue, long timeStamp);
          void conditionOccured(Object cookie, long timeStamp);
      }
    

The next method, registerExpression(), is used only in combination with the EXPRESSION_CHANGED and CONDITION_OCCURED event kinds (and the corresponding methods in ContextListener). This allows clients to register to more complex events. For example, if a client only wants to be notified when the room temperature goes beyond 30°C, it can use the following code:

context.registerExpression(EnumSet.of(CONDITION_OCCURED),
                           "/geo/location/room#temperature > 30",
                           aListener);

instead of using a simpler ATTRIBUTE_CHANGED event kind, and being notified a many non-intersting events.

An EXPRESSION_CHANGED event occurs each time the value of the monitored expression changes. If the expression depends on multiple paths, WildCAT will automatically recompute its value each time one of these paths changes. CONDITION_OCCURED can be seen as a special case of EXPRESSION_CHANGED for boolean expressions where only the transation from false to true are considered.

The expressions usable with registerExpression() can use paths, constant numbers and strings, comparison operators (=, !=, <, >, <=, >=), arithmetic operations (+, -, *, /, with the usual precedence rules), boolean operators (not, and, or), and finally function invocation (function(arg1, arg2...)). WildCAT provides some predefined functions working on numbers and strings, but new functions can be added easily to extend the vocabulary of synthetic expression (this feature is probably the best way to extend the power of WildCAT without changing its external interface).

Both register() and registerExpression() return an opaque identifier ("cookie") corresponding to this particular subscription. This object should be kept by the client, as it must be provided to unregister() in order to disable an event subscription, and is also used when sending notifications to listeners.

Configuration

When first created, an instance of Context does not contain any resource or attribute. Before it can be used by a client as described in the previous section, it must be configured. This configuration can be done by the client himself, but need not be: pre-configured contexts could be made available to users, tailored to particular kinds of applications (i.e. desktop applications, application servers, administration consoles...).

In analogy with Unix filesystems, where different filesystems implementations can be integrated in a single hierarchy, configuring WildCAT is done by mounting (and unmounting) context providers at specific locations. A context provider is simply a Java object which implements the ContextProvider interface (see that interface's documentation for more details). As a framework, WildCAT is only concerned with the integration of such providers, and not on their actual implementation or configuration: each specific implementation provides its own configuration mechanism.

      Context ctx = new Context();
      MyProvider p1 = new MyProvider();
      // configure p1
      ctx.mount("/some/location", p1);
      // change p1's configuration
      SomeOtherProvider p2 = new SomeOtherProvider("configuration.file");
      ctx.mount("/another/location", p2);
    

However, WildCAT provides nonetheless two flexible and configurable implementations of ContextProvider:

Extension: writing adapters and providers

WildCAT distinguishes two kinds of extensions: adapters and providers.

Adapters are simple wrappers for existing sensors, to make them usable by DynamicContextProvider. There are used when the existing monitoring technology to integrate has a simple, non-structured data model. For example, an adapter exists for LeWYS probes (not included in the default distribution), as these probes provide their data using a simple model and are independent of each others.

Providers are more complex: they provide a full implementation of the ContextProvider interface (maybe using one of the existing implementation as a basis). They are used when the technology to intergrate already provides a structured data model (e.g. HAL).

The default WildCAT distribution does not include any adapter or provider (except for an example FileSystemProvider) in ordre to keep its dependencies minimal. Several already exist or are in the work however, and will be distributed separately as extensions when they mature.