By creating DModel instances in Dinemic framework your application creates each time small updates broadcasted over network. Each of this updates modifies databases of all neighbors in network.

The way how your application process such updates, which are accepted and which are rejected could shape your local database. Also this approach makes Dinemic framework different from other decentralized databases. Your copy of database could be sightly different for your application than database on neighboring network node.

Also each update incoming from neighbor nodes could modify your copy of database.

Listeners

Independently who issued update, your application could react on such change in custom way. Main two purposes of DAction is to:

  • react on change in database and adjust non-database resources, like allocating certain resources
  • protect your existing database from changes made by unauthorized tenants in network

To create listener, just create new class and inherit it:

#include <libdinemic/daction.h>

class MyAction : public Dinemic::DAction
{
public:
    void on_created(Dinemic::DActionContext &context, const std::string &key);
    void on_update(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);
    void on_owned_remove(Dinemic::DActionContext &context);
};

Above example will handle creting new object, updating it’s field and removing whole object. By defining custom methods from above MyAction class you could perform custom actions.

Full specification, what could be performed and when is like in below code snippet:


#include <libdinemic/daction.h>

class MyAction : public Dinemic::DAction
{
public:
    // Before object is created
    void on_create(Dinemic::DActionContext &context, const std::string &key);

    // After object was created
    void on_created(Dinemic::DActionContext &context, const std::string &key);

    // After object was created and is owned by local machine
    void on_owned_created(Dinemic::DActionContext &context, const std::string &key);

    // Before update. This is called on each node in network
    void on_update(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);
    
    // Before update was done and signed by authorized by object key
    void on_authorized_update(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);

    // Before update was not signed by authorized key
    void on_unauthorized_update(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);

    // Before update was done. Object is owned by this node
    void on_owned_update(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);

    // Updated
    void on_updated(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);
    void on_authorized_updated(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);
    void on_unauthorized_updated(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);
    void on_owned_updated(Dinemic::DActionContext &context, const std::string &key, const std::string &old_value, const std::string &new_value);

    // Delete
    void on_delete(Dinemic::DActionContext &context, const std::string &key, const std::string &value);
    void on_authorized_delete(Dinemic::DActionContext &context, const std::string &key, const std::string &value);
    void on_unauthorized_delete(Dinemic::DActionContext &context, const std::string &key, const std::string &value);
    void on_owned_delete(Dinemic::DActionContext &context, const std::string &key, const std::string &value);

    // Deleted
    void on_deleted(Dinemic::DActionContext &context, const std::string &key, const std::string &value);
    void on_authorized_deleted(Dinemic::DActionContext &context, const std::string &key, const std::string &value);
    void on_unauthorized_deleted(Dinemic::DActionContext &context, const std::string &key, const std::string &value);
    void on_owned_deleted(Dinemic::DActionContext &context, const std::string &key, const std::string &value);

    // Remove
    void on_remove(Dinemic::DActionContext &context);
    void on_authorized_remove(Dinemic::DActionContext &context);
    void on_unauthorized_remove(Dinemic::DActionContext &context);
    void on_owned_remove(Dinemic::DActionContext &context);
};

Applying listener to SyncInterface and limiting it to certain objects

Now we got definitinon what to do when certain type of update is being processed by Dinemic framework. Next step is to define which objects and/or fields should trigger this action:

MyListener listener;

sync->add_on_create_listener(&listener, "MyModel:[id]");
sync->add_on_update_listener(&listener, "MyModel:[id]:*");
sync->add_on_updated_listener(&listener, "MyModel:[id]:value_some_field");

Above code will connect any change related to object named MyModel with any ID and call on_create method from our listener. Second line will connect any update of model with listener’s on_update method. Third line forces triggering action on_updated after field’s value is applied on local database. This is also limited to be applied only when property some_field is changed.

To limit listeners to fields, it is necessary to add field_ in filter rule. Then you could put field name, as in above exmple, or asterisk. To apply listener on list, just add list_ before list name. To work with dictionaries, add dict_.

  • ModelName:* – this will catch all updates and creations of model and any its field
  • ModelName:[id] – this will catch only model with any IDs. This rule is not being triggered when fields/lists/dicts are updated
  • ModelName:[id]:* – this will catch any on_update/on_updated/on_delete/on_deleted method of listener on any object of ModelName. This will not catch on_create/on_created.
  • ModelName:abcdefkfoaer:* – this will catch changes only in ModelName object with ID abcdefkfoaer
  • ModelName:[id]:list_* – this will catch only updates on any lists in ModelName
  • ModelName:[id]:list_test – this will catch updates on list_test of model ModelName
  • ModelName:[id]:value_some_field – this will catch updates of field some_field of ModelName

Action order and types

When creating new listener, the DAction instance, you could define multiple versions of update, updated or other action, where update was authorized, or object is owned by local node.

First important thing is to distinguish all following methods:

  • on_… – general action, which is always triggered when listener and action matches. Additionally following methods could be triggered
  • on_…_authorized – update has valid signature made by one ofobjects listed in authorized_objects
  • on_…_unauthorized – update has no valid signature or was not signed at all
  • on_…_owned – update was done on object, which keys are present on local node. In properly designed Dinemic application only one node in network should call this method. This could be used to manage resources, which are owned by single node. On_…_owned could be triggered together with on_…_authorized.

Good practice when creating dinemic framework is to properly split logics updating database and updating resources. Be calling following code on model, you can’t be sure that update won’t be rejected by one of existing listeners:

UserModel user(...);
user.set('available_quota', 1234);

// Perform some disk allocation here

If in above example we got another listener, that checks quota availablility on system, that listener could prevent applying such update if no disk space is available. To write such code properly we should change it to following:

class AllocationListener(Dinemic::DAction) {
public:
    on_authorized_updated(...) {
        // Perform some disk allocation here
    }
}
sync->add_on_updated_listener("UserModel:[id]:value_available_quota");

UserModel user(...);
user.set("available_quota", 1234);

Above example moves the logics changing disk allocation to the listener. Due this change we could be sure, that database update was applied on our local database and no other listener prevented this update from being applied. Also if anybody with authorized key will update this field of model, our listener will perform allocation again.

Above exampel could be applied to any resource or system interaction, but shows main idea of managing resources and reacting on database changes in Dinemic framework.

Protecting update from being applied

Once we got listener and something goes wrong, or database update should not be applied, we could throw an exception to prevent SyncInterface from processing next updates associated with this change:

class AllocationListener(Dinemic::DAction) {
public:
    on_authorized_updated(...) {
        if (...) {
            throw DUpdateRejected("No available space on my disk");
        }
    }
}