Safeguarding against concurrent modifications - fieldenms/tg GitHub Wiki

  1. Overview
  2. Potentially vulnerable entities
  3. Safeguarding mechanism
    1. 1. A persistent monitor
    2. 2. Guarding vulnerable operations with the monitor
  4. When the guarded operation modifies the monitor entity

Overview

This document provides guidance on managing concurrent modifications of entities. It covers one-2-many entity relationships, and specifically concurrent modifications of the “many” side which affect either the “one” side or each other. It 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 standard conflict resolution mechanism.

Potentially vulnerable entities

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. In those cases, concurrency issues are prevented 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.

Safeguarding mechanism

Safeguarding against concurrent modifications consists of two elements:

  1. A persistent monitor.

  2. Guarding vulnerable operations with the monitor.

1. A persistent 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 = false in @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() and batchDelete(). Care should be taken to make sure that all overridden batchDelete* methods are adjusted to perform recalculation in each.

  • A system property, introduced solely to act as a monitor.

    This property could be a simple Integer counter, semantically equivalent to a version of the collection of records for the "many" side of a one-2-many relationship:

    @MapTo(autoConflictResolution = false)
    private Integer manySideVersion;

    This property will need to be incremented on save of the “many” side entity. Generally speaking, there is no need to increment the monitor upon deletion (if deletion is supported for the "many" side entity). The removal of a record should not lead to data integrity violations, and it seems unreasonable to require incrementing the monitor upon deletion for every one-2-many association. However, it is possible that in some circumstances this might be necessary.

Note: in the unlikely extreme case where many such monitor 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.

2. Guarding vulnerable operations with the monitor

"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-2-many association between MeterReading and Equipment. The saving of the "many" side, MeterReading, is a vulnerable operation, as it includes the following:

  1. Ensure that this reading is greater than the previous one and less than the next.
  2. Recalculate the total readings (update MeterReading.totalReading for 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: retrieve the monitor and update the monitor property.
    final var equipment$ = co$(Equipment.class).findByEntityAndFetch(EquipmentCo.FETCH_MODEL, 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.)

When the guarded operation modifies the monitor entity

Sometimes the vulnerable operation itself must modify the monitor entity, beyond merely updating the monitor property. For example, saving or deleting a MeterReading must also update persistent property Equipment.lastMeterReading on the "one" side to the most recent reading.

Rule: if entity E is used as the monitor for operation F, and F needs to modify E, then F must modify the same instance of E that is used as the monitor.

This follows from the shape of a guarded scope, which already ends by saving the retrieved monitor instance. If F's modification is applied to that instance, the closing save persists both the monitor update and F's change together -- in a single operation, under a single conflict check.

In contrast, retrieving a separate instance of E to modify and save on its own within F is incorrect. E will be saved twice, resulting in a self-inflicted conflict.

The following operation sequence illustrates the problem:

  1. Let E be a persisted entity record that will be used as a monitor. Let N be its version.
  2. Begin guarded scope: retrieve E as an instance e1, and modify it (e.g., increment a counter property).
  3. Perform operation F, which retrieves E as its own, separate instance e2, modifies and saves it. The persisted version of E becomes N+1.
  4. End guarded scope: save e1. This results in a conflict: the persisted version of E is N+1, while e1.version = N.

The conflict could have been avoided if e1 was shared throughout (i.e., if F used e1 instead of its own e2).

Extending the previous example of MeterReading, the following illustrates the correct approach (added lines are marked with (+)):

public MeterReading save(final MeterReading reading) {
  // Begin guarded scope: retrieve the monitor and update the monitor property.
  final var equipment$ = co$(Equipment.class).findByEntityAndFetch(EquipmentCo.FETCH_MODEL, reading.getEquipment());
  equipment$.incReadingChangeCounter();

  // Validation.
  validateReading(reading).ifFailure(Result::throwRuntime);

  // Update total readings.
  ...

  // Save the reading.
  final var savedReading = super.save(reading);

  // (+) F modifies E: recompute lastMeterReading on the SAME instance used as the monitor.
  final var lastReading = findLastReadingForEquipment(equipment$);
  equipment$.setLastMeterReading(lastReading);

  // End guarded scope: a single save now persists BOTH the counter bump and lastMeterReading.
  // 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;
}

Because readingChangeCounter and lastMeterReading are both set on equipment$, the closing save(equipment$) writes them in one operation and one version check.

If several vulnerable operations share this pattern, it is worth factoring the guarded scope into a helper that retrieves and increments the monitors, runs the operation, and saves the monitors -- passing the retrieved monitor instances into the operation so that it can apply its modifications to them.

<R> R callWithMonitors(final List<MeterReading> readings, final Function<List<Equipment>, R> fn) {
  // Retrieve equipments associated with the readings.
  // Each equipment acts as a monitor to protect against concurrent modification of readings.
  final List<Equipment> equipments = ...;
  equipments.forEach(Equipment::incReadingChangeCounter)

  // Pass equipments to the function so that it can modify them if necessary.
  final var result = fn.apply(equipments);

  final var co$Equipment = co$(Equipment.class);
  equipments.forEach(equipment -> {
    // Save the equipment, which will fail if there were concurrent changes to readings.
    try {
      co$Equipment.save(equipment);
    } catch (final EntityCompanionException ex) {
      if (ex.getMessage().startsWith(PersistentEntitySaver.ERR_COULD_NOT_RESOLVE_CONFLICTING_CHANGES)) {
        throw Modules.EQUIPMENT.newException(ERR_CONCURRENT_CHANGES_TO_METER_READINGS.formatted(equipment));
      }
      throw ex;
    }
  });

  return result;
}

public MeterReading save(final MeterReading reading) {
  return callWithMonitors(List.of(reading), equipments -> {
    // Validation.
    validateReading(reading).ifFailure(Result::throwRuntime);

    // Update total readings.
    ...

    // Save the reading.
    final var savedReading = super.save(reading);

    // Recompute lastMeterReading on the SAME instance used as the monitor.
    equipments.forEach(equipment -> {
      final var lastReading = findLastReadingForEquipment(equipment);
      equipment.setLastMeterReading(lastReading);
    });

    return savedReading;
  });
}
⚠️ **GitHub.com Fallback** ⚠️