Files

156 lines
9.2 KiB
Markdown
Raw Permalink Normal View History

2026-04-17 14:55:32 -04:00
---
source_files:
- Common/DTS.Common.Import/DatabaseLocks/LockImportGroups.cs
- Common/DTS.Common.Import/DatabaseLocks/LockImportTestSetups.cs
- Common/DTS.Common.Import/DatabaseLocks/LockImportSensors.cs
generated_at: "2026-04-16T02:06:50.106613+00:00"
model: "Qwen/Qwen3-Coder-Next-FP8"
schema_version: 1
sha256: "95ed39e21f5416a3"
---
# Documentation: Database Locking for Import Operations
## 1. Purpose
This module provides database-level locking for import operations on three distinct entity types—**Groups**, **Test Setups**, and **Sensors**—to prevent concurrent modifications during data import workflows. Each class (`LockImportGroups`, `LockImportTestSetups`, `LockImportSensors`) implements the `ILockImport` interface and encapsulates logic to acquire, release, and (if authorized) steal locks on the respective entities. Locks are managed via the shared `LockManager` utility, with support for detecting and reclaiming *stranded locks* (i.e., locks held longer than a configurable timeout) and handling contention by requiring explicit admin consent. These classes exist to ensure data integrity and consistency during import processes that modify critical system resources.
## 2. Public Interface
All three classes implement the same interface `ILockImport`, with identical method signatures and semantics. Below are the concrete implementations per class.
### `LockImportGroups`
- **Constructor**:
`LockImportGroups(User currentUser, double strandedLockTimeoutMinutes)`
Initializes the instance with the current user and timeout (in minutes) after which a lock is considered stranded.
- **`bool Contended { get; }`**
Returns `true` if any group lock acquisition failed due to contention (i.e., another user holds the lock and it is not expired or owned by the current user).
- **`void FreeLock(ref ImportObject importObject)`**
Releases all locks currently held by this instance (stored in `_lockedGroups`). Iterates over `_lockedGroups` and calls `LockManager.FreeLock(...)` for each. No-op if `_lockedGroups` is empty.
- **`void SetLock(ref ImportObject importObject, ref StringBuilder message)`**
Attempts to acquire locks for all groups returned by `importObject.StaticGroups()`.
- Clears `_lockedGroups` and `_contendedGroups` first.
- For each group:
- Calls `LockManager.LockItem(...)` with category `LockManager.ItemCategories.Group`.
- On failure:
- Skips if `lockError.ErrorCode == LockError.ITEM_NOT_FOUND`.
- Otherwise, checks if lock is expired (`LastUpdated + timeout < Now`) or owned by current user/machine. If so, frees and re-acquires the lock, adding to `_lockedGroups`.
- Else, adds the existing lock record to `_contendedGroups`.
- On success, adds lock record to `_lockedGroups`.
- If any locks are contended, appends a user-facing message listing each contended group (key, owner, machine) using `StringResources.ImportTestSetup_GroupsLocked`.
- **`bool StealLock(bool proceed)`**
Attempts to steal contended locks (only if `proceed == true` and `_currentUser.IsAdmin == true`).
- Returns `true` if no contention or if stealing succeeded.
- Returns `false` if user is not admin or `proceed` is `false`.
- For each contended group:
- Calls `LockManager.FreeLock(...)` to release the existing lock.
- Calls `LockManager.LockItem(...)` again (note: category incorrectly set to `LockManager.ItemCategories.Sensor`—see *Gotchas*).
- Adds the new lock record to `_lockedGroups`.
### `LockImportTestSetups`
- **Constructor**:
`LockImportTestSetups(User currentUser, double strandedLockTimeoutMinutes)`
Same semantics as `LockImportGroups`.
- **`bool Contended { get; }`**
Same semantics.
- **`void FreeLock(ref ImportObject importObject)`**
Releases locks in `_lockedTests` via `LockManager.FreeLock(...)`.
- **`void SetLock(ref ImportObject importObject, ref StringBuilder message)`**
Attempts to lock all test setups returned by `importObject.TestSetups()`, using category `LockManager.ItemCategories.TestSetup`.
- On contention, appends message using `StringResources.ImportTestSetup_TestsLocked`.
- **`bool StealLock(bool proceed)`**
Same logic as `LockImportGroups.StealLock(...)`, but operates on `_contendedTests` and `_lockedTests`.
### `LockImportSensors`
- **Constructor**:
`LockImportSensors(User currentUser, double strandedLockTimeoutMinutes)`
Same semantics.
- **`bool Contended { get; }`**
Same semantics.
- **`void FreeLock(ref ImportObject importObject)`**
Releases locks in `_lockedSensors` via `LockManager.FreeLock(...)`.
- **`void SetLock(ref ImportObject importObject, ref StringBuilder message)`**
Attempts to lock all sensors referenced in `importObject.Sensors()`.
- Pre-builds a `sensorLookup` from `SensorsCollection.SensorsList.GetAllSensors(true)` keyed by `SerialNumber`.
- Skips sensors not in the lookup.
- Uses category `LockManager.ItemCategories.Sensor`.
- On contention, appends message using `StringResources.ImportTestSetup_TestsLocked` *(note: same string as test setups—see *Gotchas*)*.
- **`bool StealLock(bool proceed)`**
Same logic as others, but operates on `_contendedSensors` and `_lockedSensors`.
## 3. Invariants
- **Lock ownership and expiration**:
A lock is considered reclaimable if either:
- `existingLock.LastUpdated.AddMinutes(_strandedLockTimeoutMinutes) < DateTime.Now`, *or*
- `existingLock.LockingUserName == _currentUser.UserName && existingLock.LockingMachineName == Environment.MachineName`.
- **Lock state separation**:
After `SetLock(...)` completes, `_lockedGroups`/`_lockedTests`/`_lockedSensors` contains *only* locks successfully acquired (including re-claimed ones), and `_contendedGroups`/`_contendedTests`/`_contendedSensors` contains *only* locks that could not be acquired due to active contention.
- **`Contended` property**:
Always reflects whether `_contended*` list is non-empty *after* the last `SetLock(...)` call.
- **Admin requirement for stealing**:
`StealLock(...)` returns `false` unless `_currentUser.IsAdmin == true` *and* `proceed == true`.
- **No double-locking**:
`SetLock(...)` clears the internal lists before processing, ensuring no stale lock records persist across calls.
## 4. Dependencies
### External Dependencies
- **`DTS.Common.Classes.Locking.LockManager`**: Central lock manager used for `LockItem`, `FreeLock`. Defines `LockError`, `LockRecord`, and `ItemCategories`.
- **`DTS.Slice.Users.User`**: Represents the current user; accessed for `UserName`, `Id`, and `IsAdmin`.
- **`DTS.Common.Storage.LockManager`**: Used in `LockImportSensors` to access `SensorsCollection.SensorsList.GetAllSensors(...)`.
- **`DTS.Common.SharedResource.Strings.StringResources`**: Provides localized messages (e.g., `ImportTestSetup_GroupsLocked`, `ImportTestSetup_TestsLocked`).
- **`DTS.Common.Utilities.Logging.APILogger`**: Used to log exceptions during lock operations.
### Interface Dependencies
- **`ILockImport`**: All three classes implement this interface (not shown in source, but implied by usage and naming). Signature must include `SetLock`, `FreeLock`, `StealLock`, and `Contended`.
### Depended Upon
- These classes are likely used by higher-level import orchestrators (e.g., `ImportService`, `ImportWizard`) that coordinate import workflows and handle user prompts for lock stealing.
## 5. Gotchas
- **Incorrect category in `StealLock` for Groups**:
In `LockImportGroups.StealLock(...)`, the re-lock call uses `LockManager.ItemCategories.Sensor` instead of `LockManager.ItemCategories.Group`. This is inconsistent with the initial lock and may cause lock metadata corruption or misattribution.
- **Incorrect resource string for sensor contention messages**:
In `LockImportSensors.SetLock(...)`, the message appended on contention uses `StringResources.ImportTestSetup_TestsLocked` (same as test setups), not a sensor-specific string. This may mislead users.
- **No validation of `ImportObject` contents**:
The classes assume `importObject.StaticGroups()`, `TestSetups()`, and `Sensors()` return valid collections. No null/empty checks are performed on the collections themselves.
- **Exception swallowing in `SetLock`**:
All exceptions during lock acquisition are caught and logged via `APILogger.Log(ex)`, but the method continues silently. This may hide critical failures (e.g., database connectivity issues) and leave the system in an inconsistent state (e.g., partial locking).
- **Race condition in lock stealing**:
Between `FreeLock` and `LockItem` in `StealLock(...)`, another user could acquire the lock. The code does not re-check lock ownership before re-locking.
- **Ambiguity in `ImportObject` interface**:
The source does not define `ImportObject` or its methods (`StaticGroups`, `TestSetups`, `Sensors`). Their behavior (e.g., ordering, nullability) is unknown.
- **No rollback on partial failure**:
If `SetLock(...)` fails partway through (e.g., due to an exception), already-locked items are *not* released. Only `FreeLock(...)` (called later) cleans up.
- **Machine name dependency**:
Lock ownership check uses `Environment.MachineName`, which may be unreliable in containerized/distributed environments or if machine names change.
None identified beyond the above.