Subscribing

PPS clients can subscribe to multiple objects, and PPS objects can have multiple subscribers. When a publisher changes an object, all clients subscribed to that object are informed of the change.

To subscribe to an object, a client simply calls open() for the object with O_RDONLY to subscribe only, or O_RDWR to publish and subscribe. The subscriber can then query the object with a read() call.

A read returns the length of the data read, in bytes. If the allocated read buffer is too small for the data being read in, the read fails.

Blocking and non-blocking reads

By default, reads to PPS objects are non-blocking; that is, PPS defaults a normal open() to O_NONBLOCK, and reads made by the client that opened the object do not block. This behavior is atypical for most filesystems. It is implemented so that standard utilities will not hang waiting for a change when they make a read() call on a file.

For example, with the default behavior, you could tar up the entire state of PPS using the standard tar utility. Without this default behavior, however, tar would never make it past the first file opened and read.

Setting PPS to block

Though the PPS default is to open objects for non-blocking reads, the preferred method for querying PPS objects is to use blocking reads. With this method, a read waits until the object or its attributes change, then returns data.

To have reads block, you need to open the object with the ?wait pathname open option, appended as a suffix to the pathname for the object. For example:

For information about ?wait and other pathname open options, see the chapter Options and Qualifiers.

A typical loop in a subscriber would live in its own thread. For a subscriber that used the opened the object with the ?wait option, this loop might do the following:

/* Assume that the object was opened with the ?wait option
   No error checking in this example. */
for(;;) {
    read(fd, buf, sizeof(buf)); // Read waits until the object changes.
    process(buf);
}

Clearing O_NONBLOCK

If you have opened an object without the ?wait option, and want to change to blocking reads, you can clear the O_NONBLOCK bit, so that the subscriber waits for changes to an object or its attributes.

To clear the bit you can use the fcntl() function. For example:

flags = fcntl(fd, F_GETFL);
flags &= ~O_NONBLOCK;
fcntl(fd, F_SETFL, flags);

Or you can use the ioctl() function:

int i=0;
ioctl(fd,FIONBIO,&i);

After clearing the O_NONBLOCK bit, you can issue a read that waits until the object changes.

io_notify() functionality

The PPS service implements io_notify() functionality, allowing subscribers to request notification via a PULSE, SIGNAL, SEMAPHORE, etc. On notification of a change, a subscriber must issue a read() to the object file to get the contents of the object. For example:

/* Process events while there are some */
while(ionotify(fd, _NOTIFY_ACTION_POLLARM, _NOTIFY_COND_INPUT,
    &event) & _NOTIFY_CONT_INPUT) {
	if(read(fd, buf, sizeof(buf)) > 0) // Best to read with O_NONBLOCK
    process(buf);
    }
/* The event will be triggered in the future to get our attention */

Getting notifications of data on a file descriptor

You can use either one of two simple mechanisms to receive notifications that data is available on a file descriptor:

Subscription Modes

A subscriber can open an object in full mode, in delta mode, or in full and delta modes at the same time. The default is full mode. To open an object in delta mode, you need to open the object with the ?delta pathname open option, appended as a suffix to the pathname for the object.

For information about ?delta and other pathname open options, see the chapter Options and Qualifiers.

Full mode

In full mode (the default), the subscriber always receives a single, consistent version of the entire object as it exists at the moment when it is requested.

If a publisher changes an object several times before a subscriber asks for it, the subscriber receives the state of the object at the time of asking only. If the object changes again, the subscriber is notified again of the change. Thus, in full mode, the subscriber may miss multiple changes to an object — changes to the object that occur before the subscriber asks for it.

Delta mode

In delta mode, a subscriber receives only the changes (but all the changes) to an object's attributes.

On the first read, since a subscriber knows nothing about the state of an object, PPS assumes everything has changed. Therefore, a subscriber's first read in delta mode returns all attributes for an object, while subsequent reads return only the changes since that subscriber's previous read.

Thus, in delta mode, the subscriber always receives all changes to an object.

The figure below illustrates the different information sent to subscribers who open a PPS object in full mode and in delta mode.


PPS subscription modes


Comparison of PPS full and delta subscription modes.

In all cases PPS maintains persistent objects with states — there is always an object. The mode used to open an object does not change the object; it only determines the subscriber's view of changes to the object.

Delta mode queues

When a subscriber opens an object in delta mode, the PPS service creates a new queue of object changes. That is, if multiple subscribers open an object in delta mode, each subscriber has its own queue of changes to the object, and the PPS service sends each subscriber its own copy of the changes. If no subscriber has an object open in delta mode, the PPS service does not maintain any queues of changes to that object.


Note: On shutdown, the PPS service saves its objects, but objects' delta queues are lost.

Changes to multiple attributes

If a publisher changes multiple attributes with a single write() call, then PPS keeps the deltas together and returns them in the same group on a subscriber's read() call. In other words, PPS deltas maintain both time and atomicity of changes. For example:

write()                     write()
  time::1.23                 time::1.24
  duration::4.2              write()
                             duration::4.2

read()                     read()
  @objname                   @objname
  time::1.23                 time:1.24
  duration::4.2              @objname
                             duration::4.2

Server objects

When a client writes to a server object, only the application that created it with the ?server option (called the "server") will get the message. Other clients cannot see that message.

At write-time, PPS appends a unique identifier to the object name so that the "server" knows which client connection is sending the message. This allows the connection to have stateful information. For example:

@foo.1234

indicates object foo with client identifier 1234. When a client connects, the server reads a new object that is prefixed with a + symbol (for example, +@foo.1234). Disconnects are sent to the "server" and the + prefix is changed to a - prefix.

When a server replies, it must write the object name with the unique identifier appended so that the response is sent only to the client that is identified by the unique identifier. If a server does not append the unique identifier to the object name, the message will be broadcast to all clients that are connected to the object.

When an object is opened with the ?server option it automatically becomes a critical publisher of that object. It also automatically receives notifications in delta mode.

The following figure shows a PPS transaction using the ?server option:


A PPS server transaction


Using the ?server option

Subscribing to multiple objects

PPS supports three special objects which facilitate subscribing to multiple objects:

Subscribe to all objects in a directory

PPS uses directories as a natural grouping mechanism to simplify and make more efficient the task of subscribing to multiple objects. Subscribers can open multiple objects, either by calling open() then select() on their file descriptors. More easily, they can open the special .all object, which merges all objects in its directory.

For example, assume the following object file structure under /pps:

rear/left/PlayCurrent
rear/left/Time
rear/left/PlayError

If you open rear/left/.all you will receive a notification when any object in the rear/left directory changes. A read in full mode will return at most one object per read.

read()
@Time
  position::18
  duration::300

read()
@PlayCurrent
  artist::The Beatles
  genre::Pop
  ... the full set of attributes for the object

If you open a .all object in delta mode, however, you will receive a queue of every attribute that changes in any object in the directory. In this case, a single read() call may include multiple objects.

read()
@Time
  position::18
@Time
  position::19
@PlayCurrent
  artist::The Beatles
  genre::Pop

Notification groups

PPS provides a mechanism to associate a set of file descriptors with a notification group. This mechanism allows you to read only the PPS special notification object to receive notification of changes to any of the objects associated with that notification group.

Creating notification groups

To create a notification group:

  1. Open the .notify object in the root of the PPS file system.
  2. Read the .notify object; the first read of this file returns a short string (less than 16 characters) with the name of the group to which other file descriptors should associate themselves.

To associate a file descriptor to a group, on an open, specify the pathname open option ?notify=group:value, where:


Note: The returned notification group string has a trailing linefeed character, which you must remove before using the string.

For information about ?notify and other pathname open options, see the chapter Options and Qualifiers.

Using notification groups

Once you have created a notification group and associated file descriptors to it, you can use this group to learn about changes to any of the objects associated with it.

Whenever there is data available for reading on any of the group's file descriptors, reads to the notification object's file descriptor return the string passed in the ?notify=group:value pathname option.

For example, with PPS is mounted at /pps, you could write something like the following:

char noid[16], buf[128];
int notify_fd, fd1, fd2;

notify_fd = open("/pps/.notify", O_RDONLY);
read(notify_fd, &noid[0], sizeof(noid));

sprintf(buf, "/pps/fish?notify=%s:water", noid);
fd1 = open(buf, O_RDONLY);
sprintf(buf, "/pps/dir/birds?notify=%s:air", noid);
fd2 = open(buf, O_RDONLY);

while(read(notify_fd, &buf, sizeof(buf) > 0) {
    printf("Notify %s\n", buf);
}

The data printed from the “while” loop in the example above would look something like the following:

Notify 243:water
Notify 243:water
Notify 243:air
Notify 243:water
Notify 243:air

Note: When reading from an object that is bound to a notification group, a subscriber should do multiple reads for each change indicated. There may be more than one change on an item, but there is no guarantee that every change will be indicated on the notification group's file descriptor.

Notification of closed file descriptors for objects

If a file descriptor for an object which is part of a notification group is closed, the string passed with the change notification is prefixed by a minus (“-”) sign. For example:

-243:air