Idempotency and Guard Rules
Rule systems often work better when responses to an event are idempotent, meaning that they can run multiple times without cumulative effect.
Guard conditions in rules are usually the best way to ensure idempotency in rules because they can be used to only have an effect when specific conditions are met.
A guard rule offers a useful method for assuring idempotency in rules. The basic idea is to create two rules: one that tests a guard condition and one that carries out the rule's real purpose.
The guard rule:
responds to the event
tests a condition that ensures idempotence
raises an explicit event in the postlude that the second rule is listening for.
For example, in the Fuse system, we want to ensure that each owner has only one fleet.
The following is the guard rule for the Fuse initialization (assuming pds is a module providing persistent storage for multiple rule sets):
rule kickoff_new_fuse_instance {
select when fuse need_fleet
pre {
fleet_channel = pds:get_item(common:namespace(),"fleet_channel")
is_null = fleet_channel.isnull()
}
if(is_null) then
send_directive("requesting new Fuse setup");
fired {
raise explicit event "need_new_fleet"
attributes {
"_api" : "sky",
"fleet" : event:attr("fleet") || "My Fleet"
}
log warn <<Not creating new fleet; fleet channel exists: #{fleet_channel}" if is_null
}
}The guard rule only continues if the fleet channel is null (evidence that a fleet doesn't already exist).
The rule below needs updating from the way the old pico engine did things.
The second rule does the real work of creating a fleet pico and initializing it.
rule create_fleet {
select when explicit need_new_fleet
pre {
fleet_name = event:attr("fleet");
pico = common:factory({"schema": "Fleet", "role": "fleet"}, meta:eci());
fleet_channel = pico{"authChannel"};
fleet = {"cid": fleet_channel};
pico_id = "Owner-fleet-"+ random:uuid();
}
if (pico{"authChannel"} neq "none") then
every {
send_directive("Fleet created", {"cid":fleet_channel});
// tell the fleet pico to take care of the rest of the initialization.
event:send(fleet, "fuse", "fleet_uninitialized") with
attrs = {"fleet_name": fleet_name,
"owner_channel": meta:eci(),
"schema": "Fleet",
"_async": 0 // we want this to be complete before we try to subscribe below
};
}
fired {
// put this in our own namespace so we can find it to enforce idempotency
raise pds event new_data_available
with namespace = common:namespace()
and keyvalue = "fleet_channel"
and value = fleet_channel
and _api = "sky";
// make it a "pico" in CloudOS eyes
raise cloudos event picoAttrsSet
with picoChannel = fleet_channel // really ought to be using subscriber channel, but don't have it...
and picoName = fleet_name
and picoPhoto = common:fleet_photo
and picoId = pico_id
and _api = "sky";
// subscribe to the new fleet
raise cloudos event "subscribe"
with namespace = common:namespace()
and relationship = "Fleet-FleetOwner"
and channelName = pico_id
and targetChannel = fleet_channel
and _api = "sky";
log ">>> FLEET CHANNEL <<<<";
log "Pico created for fleet: " + pico.encode();
raise fuse event new_fleet_initialized;
} else {
log "Pico NOT CREATED for fleet";
}
}
When this rule fires, an action sends an event to the newly created fleet pico that causes it to initialize and three events are raised in the postlude that cause further initialization to take place.