Safeguarding against concurrent modifications - fieldenms/tg GitHub Wiki
This wiki does not apply to concurrent modifications of any single entity. This aspect is already well handled by the TG platform automatically through versioning and the conflict resolution mechanism.
This wiki covers one-2-many entity relationships, and specifically the concurrent modifications of the “many” side of the relationship which affect either the “one” side or each other.
Entities, which contain neither a Date property nor an auto-generated number, GUID or something similar as one of their key members, are typically not vulnerable to concurrent modifications.
Examples of such entities:
-
PmXreforPmTimeXref -
InventorySupplier -
WorkActivityExpendable
In all these cases, concurrency issues are prevented by the model, specifically — by the business key.
The need for safeguarding against concurrent modifications, therefore, should be carefully considered for entities which include a Date property, a User property, or other automatically assigned value as one of their key members.
Examples of such entities:
-
InventoryIssue--transactionDateis auto-assigned -
<R extends AbstractReceipt<R, ?>> -
PositionAllocation -
LocationSubsystemTimeline
Safeguarding against concurrent modifications consists of two elements:
-
A persistent monitor.
-
Guarding vulnerable operations with the monitor.
The essence of a monitor is that its value cannot be changed concurrently. A monitor must be persisted so that we can leverage TG's existing support for detecting concurrent modifications.
A persistent property of the "one" side is frequently used as a monitor.
Common choices include:
-
A persistent property that represents some kind of aggregation of the “many” side.
To make such a property act as a monitor, it is necessary to turn automatic conflict resolution off by setting
autoConflictResolution = falsein@MapTo.If such a property exists, but is calculated, it may be converted into a persistent one, but will also need to be carefully recalculated upon saving and deletion. Care should be taken to recalculate the value both upon
save()andbatchDelete(). Care should be taken to make sure that all overriddenbatchDelete*methods are adjusted to perform recalculation in each. -
A system property, introduced solely to act as a monitor.
This property could be a simple
Integerversion counter:@MapTo(autoConflictResolution = false) private Integer manySideVersion;
It will only need to be updated on save of the “many” side, not deletion. (TODO: Why?)
In the unlikely extreme case where many such system properties are required, a separate one-2-one synchronisation entity, such as
WorkActivitySync, should be introduced to contain all these helper properties and thus avoid polluting the “one” side entity.
"Vulnerable" here means "vulnerable to concurrent modification".
To guard an operation with a monitor is to enclose it between two operations: retrieval of the monitor object and saving of the updated monitor object.
For example, when a persistent property is used as a monitor:
-
Retrieval of the monitor object -- retrieval of the entity that has the monitor property.
-
Saving of the updated monitor object -- saving of the retrieved entity with an updated value of the monitor property.
Note that the value of the monitor property may be updated at any point between these two events. For simplicity and consistency, it is recommended to update it right after the retrieval.
Having defined the boundaries of guarded scope, it should be clear by now that vulnerable operations should be executed within that scope.
For example, consider the one-to-many association between MeterReading and Equipment.
The saving of the "many" side, MeterReading, is a vulnerable operation, as it includes the following:
- Ensure that this reading is greater than the previous one and less than the next.
- Recalculate the total readings (update
MeterReading.totalReadingfor all later readings).
Therefore, the whole operation should be executed within guarded scope.
In this example, persistent property Equipment.readingChangeCounter acts as the monitor.
@IsProperty
@Readonly
@Required
@MapTo(autoConflictResolution = false)
@Title(value = "Reading Change Counter", desc = "Prevents concurrent operations on meter readings.")
private Integer readingChangeCounter;public class MeterReadingDao ...
public MeterReading save(final MeterReading reading) {
// Begin guarded scope.
final var equipment$ = co$(Equipment.class).findByEntityAndFetch(
EquipmentCo.FETCH_MODEL.with("readingChangeCounter"),
reading.getEquipment());
equipment$.incReadingChangeCounter();
// Validation.
validateReading(reading).ifFailure(Result::throwRuntime);
// Update total readings.
...
// Save the reading.
final var savedReading = super.save(reading);
// End guarded scope.
// save() will throw if there was a concurrent modification.
// (This could be wrapped with try/catch to show a user-friendly message.)
co$(Equipment.class).save(equipment$);
return savedReading;
}For the complete implementation, please refer to MeterReadingDao.save in ports.
(There, MeterCapable is used instead of Equipment, and thus the implementation is a bit more complex.)