IocageEvent is a core structure used all across ioc. Whenever iocage faces async tasks, it communicates by yielding IocageEvent instances.
The Python 3 language feature Generators (Iterators) allows to communicate state during the runtime of a method, so that each task produces continuous feedback on its progress.
Because Python Generators block until the yielded variable was consumed, some classes (for example Jail
) provide a user-friendly abstraction of the internal Generator-class.
The folliing two snippets will have the same effect of starting a Jail:
ioc.Jail("myjail").start()
jail = ioc.Jail.JailGenerator("myjail")
for event in jail.start():
continue
Generator methods yield each event twice - once when the event begins and another time when it was finished.
While the pending
attribute on each event reflects whether it is currently in progress.
Events with the done
attribute set to True
are no longer pending, but may have exited with an error
or may have been skipped
all together.
IocageEvent Attribute | Description |
---|---|
pending | True when the event is currently occuring |
done | True when the event terminated |
error | Optional Exception of failed done events. |
skipped | True when a done event had no effect and was skipped |
It is possible that an event is still pending while other events are handled. When another event is started while another event is not finished yet, it becomes a nested child event.
From an event consumers perspective a typical stack of events provided by a generator looks like
The start command of a jail is a good example to demonstrate the consumption of events.
This time the ioc.Jail.JailGenerator
class is used directly instead of the syncronous wrapper ioc.Jail.Jail
.
jail = ioc.Jail.JailGenerator("myjail")
for event in jail.start():
if event.pending is True:
print(f"{event.type} is pending")
else:
if event.skipped is True:
status = "skipped"
elif event.error is not None:
status = "failed"
else:
status = "done"
print(f"{event.type} is {status}")
print(event.type, event.pending)
When processing stacked tasks it is important to never leave the system in a intermediate state. Whenever an event can cause such persistent changes, a rollback method is attached to the event. This method is called in case of failure of a sibling or child event (in LIFO order), so that changes can be reverted.
This abstract example shows how such a rollback step is implemented by rolling back a ZFS snapshot after a failed event:
import ioc
import libzfs
zfs = libzfs.ZFS()
def fail_doing_something(dataset_name):
snapshot_name = f"{dataset_name}@demo-snapshot"
dataset = zfs.get_dataset(dataset_name)
dataset.snapshot(snapshot_name)
snapshot = zfs.get_snapshot(snapshot_name)
def _rollback_snapshot():
snapshot.rollback()
myevent = ioc.events.IocageEvent()
myevent.add_rollback_step(_rollback_snapshot);
yield myevent.begin()
# ...
yield myevent.fail("Some error occurred")
fail_doing_something("zroot/my_dataset")