Skip to content

uevent handling design overview

dwlehman edited this page Mar 2, 2015 · 3 revisions

uevent handling in blivet

Background/Motivation

Currently, Blivet does not watch for changes made by entities other than itself. If a user makes a change from outside of blivet, blivet will remain unaware of that change until/unless the devicetree gets reset and repopulated. It would be better if blivet could detect changes made from outside and adapt to them when possible. For example, if a user creates new devices on an empty disk, blivet should update its model to include the new devices.

Another consequence of blivet's poor integration with udev is that blivet never knows for certain when udev has created device nodes for new devices. As a result, blivet calls 'udevadm settle' in several key locations to give time for udev to process its event queue. Listening for uevents would give blivet knowledge of exactly when the device node for a newly-created device appears.

Design Considerations

Scope of changes

Considerable effort was given in trying not to disrupt existing, working code. At least in the short term, I want blivet to function as it did previously if uevent handling is not enabled.

Threading

The python module pyudev provides a python wrapper around libudev, including classes for both synchronous and asynchronous monitoring of uevents. Asynchronous monitoring makes more sense since blivet will need to listen for events while doing other things. Event notifications come in separate threads, so blivet will have to be thread-safe.

I opted for a global re-entrant lock to keep the initial implementation as simple as possible. If, at some point in the future, we decide that significant performance gains are desired and may be achieved through the use of finer-grained locking we can certainly investigate that.

Events as notification of externally-initiated events

If a user makes changes to a device outside of blivet, we will not know about it, so the events we receive will be unexpected. We will have to examine the available data and decide what, if anything, has changed. Then, if a change has occurred, we must update the devicetree to reflect the changes.

Events as confirmation of blivet actions

When blivet performs an operation that changes something on disk (eg: create a new filesystem on a partition), it can expect there to be a uevent when the operation has completed (eg: a "change" uevent on the partition). With some synchronization betweeen the code performing the operation and the code handling the event, we can confirm that the operation has taken effect. We can even confirm that it was successful based on the information provided by udev along with the uevent.

Design Elements

Event

This is a class to represent an event. It has only two public attributes: device (str, eg: "sda") and action (str, eg: "change"). The hope is that we could eventually extend this model to include, for example, events on md arrays (eg: array home has become degraded).

EventQueue

This represents the queue of received events that have not yet been handled. The rationale for maintaining a queue instead of calling the handler directly is that it allows for several possible tweaks, including a delay on executing the handler to allow for aggregation and/or selective pruning of events. If we decide to abandon the potential benefits of queueing events it should be possible to eliminate this class altogether.

EventManager

This class represents a full event-handling configuration: an event queue and an event handler function. Of note are methods enable and disable, attributes handler_cb, next_event, and enabled.

blivet.flags.uevents

This is a boolean flag indicating whether or not event handling is enabled. It is set from EventManager.enable and unset from EventManager.disable.

StorageEventSynchronizer

This class is built around a threading.Condition, using it in combination with a set of mutually-exclusive boolean flags to achieve synchronization between StorageDevice/DeviceFormat methods and uevent handlers. The underlying lock used for the threading.Condition is the global blivet_lock.

Say goodbye to simplicity

Every StorageDevice instance has controlSync and modifySync attributes, which are StorageEventSynchronizer instances. Unfortunately, it is necessary to have distinct attributes for synchronization of control operations (setup, teardown) and modify operations (create, destroy). For normal devices, such as partitions, modifySync is the same as controlSync. As devices become more complex, however, things get weird. When an LVM VG is created, the only uevents are change events on the PVs because the VG itself has no block device. Similarly, when an LV is created or destroyed, the events are on the PVs. (You cannot use a remove event, because that could just as easily be a deactivation as a removal/destruction.) For this purpose, there is StorageEventSynchronizerSet, which aggregates the synchronizers for the member devices (eg: LVM PVs) into the same API as an individual StorageEventSychronizer. MD arrays use events on the members for modify and events on the array itself for control. LVM LVs use events on the PVs for modify and events on the LV itself for control. Luckily, things are simpler for formats, which have only one synchronizer attribute: eventSync.

Action/event synchronization in practice

We'll use device creation as an example. I'll refer to StorageEventSynchronizer instances as "event syncs".

  1. Before initiating whatever operation will actually create the device, we set the event sync's creating flag.
  2. The sequencing of the following two items is not completely predictable (see note below this list)
    • Once control has returned from whatever method or external utility did the creating, we use the event sync to notify the event handler that we are ready for post-processing of the operation.
    • When a uevent is received, we attempt to associate it with an active blivet operation. If this succeeds, the event handler calls the event sync's wait method, which causes the handler thread to wait until the device thread is ready to post-process the operation.
  3. The event handler notifies the device thread that the event was received and begins to wait for the device thread.
  4. The device thread sets the device's exists attribute to True, then notifies the event handler that post-processing is complete. (Some device classes may do more than just set exists to True.)
  5. The event handler proceeds normally, eg: possibly updating device UUID based on the data that accompanies the uevent.
On the race to begin synchronization:

Due to the fact that the device thread initially holds the global lock, we generally expect to call the event sync's wait method from StorageDevice._postCreate before the uevent is being handled. However, it is possible for the event handler to associate the event with the create operation before control in the device thread reaches StorageDevice._postCreate. (This is especially likely when the operation is comprised of multiple steps involving uevent synchronization because the synchronization enables the uevent handlers to run sooner than they would if the device thread held the lock during the entire create process.) The solution to this is to have the event handler do a timeout wait when it associates an event with a device create action to allow the device thread to catch up, if necessary. If the device is not already waiting for notification from the event handler, it will notify the event handler when it becomes ready. Either way, after the wait returns the synchronization process will continue under the assumption that both parties are engaged.