Entity containers targets the problem of transactions: changing multiple entity instances and attributes within a single action. The change must be "atomic", all updates must be done or discarded to keep the components in valid, consistent state - but the modification process requires time, and the operation may be interrupted by a failure (hardware, system, connection - this is the traditional transaction management) or another concurrent modification (programming language lock and synchronization constructs).
Moving the state representation to Dust means that the code should never meet this situation - it can control the "commit" / "rollback" operations, but it does not have to actively protect anything from transaction errors: it should work as configured. The encapsulation gives a way to support this, using a hierarchy of entity containers.
The Dust runtime is merely an entity instance container, with an API to get references to them, access their content and send messages. Its actual operation: the ability of handling the types, entities, messages, and that it can go for other entities upon requests are actually the functions of some required (and later time: properly protected) kernel "meta" entities.
As long as an operation accesses entities and reads their content, we have no problem, this operation can run in parallel with many components accessing the same instance. Writing causes the trouble. Of course, parallel write requests should not be allowed - but we also should not let a write operation complete while another code is actually reading the same entity, because it is nonsense to get different results to the same question (like: I make a decision because of the state of an item, and do other actions, yet the item state gets modified at the same time).
The solution (at least seems to be) quite simple, using the restrictions above. When a code uses a reference, it gets connected to its local usage pool, and also linked into the using contexts list of the aspect. When the message processing ends (the code returns), the references are released and removed from the list. When a code request a write operation, the Aspect gets locked to that code, and all concurrent write requests are suspended until this lock lives. However, the code does not change that aspect instance: it is shallow-cloned into a secondary Entity container. Whenever this code, or any other that it calls refers to this Aspect, they receive this temporal instance; but it is linked to the original container, and any instance missing from the current container will be resolved from (or by) its parent.
So, the update code collects duplicates into the local container, but does not break other processes by the parallel modification as long as it runs. When it ends, the temporary context is released and the updates are committed. This is the point when readers could be broken, so the commit process waits for all current references to be released to the item, and also blocks opening new read references to the target objects. In the root level, commit means updating the instance in its persistent storage as well.
Deadlocks
Of course, this "generic approach" has inevitable problems because of its simplicity. It can naturally result in deadlocks: parallel processes both waiting for an instance blocked by the other. However, this information is not hidden somewhere in the runtime, behind the language constructs, but clearly visible in the Dust runtime thread manager - and can be resolved there. On the other hand, the approach of atomizing the business logic and moving all state representation to Dust means that the message responding codes will be short and simple, so the synchronization problems "should" become more visible and manageable.
Releasing and listening
In longer operations the coder should take care of the consequences of having some entities locked for a longer period of time, consider the side effects and implement the behavior wisely. For example: one entity contains information that has effects on other entities. When this entity changes, many others also has to be updated. If it runs in cycle, it would gradually lock all the touched entities, which is not good - so, it can commit changes for each item thus releasing them from the local context. Of course, with the messaging architecture this is not the case: the update request can be broadcast to all members, the message processor does the update, so the change is done in the local context of the called message processor, and the local commit is done when the processor exits.
However, what should happen to the originating object? It depends. It may remain locked all along the update process to block concurrent modification. Or the relevant data can be extracted for creating the update message, and then the code can
release the entity, so even if it gets updated during the child processes, the actual modification will be coherent (though obsolete). Or, the source object can be released, but the process can
listen to its change, and if it gets updated while the process is running, it can stop the operation and restart it with the updated information again.
Operation contexts
Another typical situation is when the modification process is of "unknown length": the system enters a state of updating some entities, but has to wait for external events to happen. This is the generic scenario behind all user interfaces, server communication, interaction with a hardware component, etc. I would say that reading data from the hard drive is also the same situation.
The current programming methodology uses wait calls, spins in cycles, etc, in this case, which is again a situation when the code represents the application state, and which considered evil in Dust. The command is sent to the hardware, the information is presented to the user, so this task is done, the activated message processor code must return. The event will appear some time later, dispatched to the component again, resulting in a data change (mouse movement and click -> user interface component state change -> data modification), the relevant message processors are called, and then again the system stops and waits for the next event.
The state between these operations are stored in an operation context, a temporal persistent entity container. This is independent from Dust kernel container (which represents the "actual global state of the things"), and floats above it, containing "one required new state under construction". Such contexts can be created by some system level components - like one that can display a GUI window, open a network channel or start a stream deserialization process. When the context is closed, it can rollback the operation: throw the container, or, by default, commit it: update all entities of the parent container. Yes, this is a tree, and a window called modally from another window can update the parent window's context, and NOT the root under it (and for exchange, it can see the actual modifications made on the parent window).
This again relates to locking. The GUI displays many entities, and updates some. The update locking can break through the context depending on the configuration. The definition should contain if it
- locks the target item globally, blocking others to modify it, but ensure that no concurrent modification takes place - better for atomic components,
- or let others do parallel modification and merge the modifications on commit if needed - better for big components like source codes.
The displayed entities are another problem: they would block updates because of the read registration. An alternative is that the GUI elements listen to the displayed entity, but release it. So, the updates are allowed, but the controls get notification about it and so they can reflect to parallel changes. This is a recipe both for parallel displaying and editing the same data instances, and for screen sharing, depending on if the listening is registered to the target or the GUI element entity.