fix(external-system-gateway): resolve ExternalSystemGateway-011 — name-keyed repository lookups replace fetch-all-then-filter on the call hot path
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-16 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `9c60592` |
|
||||
| Open findings | 1 |
|
||||
| Open findings | 0 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -533,7 +533,7 @@ exception propagates; it was verified to fail before the `try/catch` was added.
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:360-374`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:169-176` |
|
||||
|
||||
**Description**
|
||||
@@ -554,26 +554,67 @@ rather than fetch-all-then-filter.
|
||||
|
||||
**Resolution**
|
||||
|
||||
2026-05-16 — **Root cause confirmed, but left Open: no correct fix is possible within
|
||||
this module's edit scope.** `ResolveSystemAndMethodAsync`
|
||||
(`ExternalSystemClient.cs:360`) does call `GetAllExternalSystemsAsync()` followed by
|
||||
`GetMethodsByExternalSystemIdAsync()` and filters in memory, and
|
||||
`ResolveConnectionAsync` (`DatabaseGateway.cs:169`) does `GetAllDatabaseConnectionsAsync()`
|
||||
then filters — fetch-all-then-filter on every hot-path call, as described.
|
||||
Resolved 2026-05-16 (commit `<pending>`). A cross-module change was explicitly
|
||||
authorized, so the **name-keyed repository lookup** recommendation was applied — the
|
||||
cleaner of the two options, and one that avoids the staleness hazard a
|
||||
deployment-invalidated cache would introduce.
|
||||
|
||||
Both recommended fixes require changes outside `src/ScadaLink.ExternalSystemGateway`:
|
||||
(a) a **name-keyed repository lookup** (e.g. `GetExternalSystemByNameAsync`) means adding
|
||||
methods to `IExternalSystemRepository` in `ScadaLink.Commons` and implementing them in
|
||||
`ScadaLink.ConfigurationDatabase` / `ScadaLink.SiteRuntime`; (b) an **in-memory cache
|
||||
invalidated on artifact deployment** requires subscribing to a deployment-applied event
|
||||
owned by `ScadaLink.SiteRuntime` / `ScadaLink.DeploymentManager`. A purely module-local
|
||||
cache with a time-based TTL was rejected as a fix: definitions only change on deployment
|
||||
and must reflect a deployment promptly, so a TTL would either be too short to help the
|
||||
hot path or long enough to serve stale definitions after a redeploy — trading a
|
||||
correctness hazard for a performance gain on a Low-severity issue. **Tracked follow-up:**
|
||||
add a name-keyed lookup to `IExternalSystemRepository` (Commons) and have the gateway use
|
||||
it, or add a deployment-invalidated definition cache wired from SiteRuntime. No source
|
||||
change was made in this module.
|
||||
Three name-keyed methods were added to `IExternalSystemRepository`
|
||||
(`ScadaLink.Commons`): `GetExternalSystemByNameAsync(name)`,
|
||||
`GetMethodByNameAsync(externalSystemId, methodName)` and
|
||||
`GetDatabaseConnectionByNameAsync(name)`. The connection lookup belongs on the same
|
||||
interface because database connection definitions are already part of
|
||||
`IExternalSystemRepository` (alongside `GetAllDatabaseConnectionsAsync` /
|
||||
`GetDatabaseConnectionByIdAsync`), so the existing repository organization was
|
||||
followed rather than introducing a new interface.
|
||||
|
||||
Both implementers of the interface were updated:
|
||||
|
||||
- `ScadaLink.ConfigurationDatabase.ExternalSystemRepository` — all three are genuine
|
||||
server-side keyed queries (`FirstOrDefaultAsync(x => x.Name == name)`, the
|
||||
method lookup additionally scoped by `ExternalSystemDefinitionId`), matching the
|
||||
existing `GetMethodByNameAsync` / `GetListByNameAsync` / `GetSharedScriptByNameAsync`
|
||||
convention in the other Central repositories.
|
||||
- `ScadaLink.SiteRuntime.SiteExternalSystemRepository` — `GetExternalSystemByNameAsync`
|
||||
and `GetDatabaseConnectionByNameAsync` are genuine single-row indexed SQLite queries
|
||||
(`WHERE name = @name`; both tables have `name` as the PRIMARY KEY).
|
||||
`GetMethodByNameAsync` resolves the named method from the parent system's
|
||||
`method_definitions` JSON column; this still requires resolving the parent system
|
||||
(one id→name scan), but the gateway's new call sequence performs **one** scan instead
|
||||
of the previous **two** (`GetAllExternalSystemsAsync` + `GetMethodsByExternalSystemIdAsync`,
|
||||
which itself scanned), so the site path is strictly better than before — noted as
|
||||
the one place a residual scan remains, bounded by the deployed system count.
|
||||
|
||||
`ExternalSystemClient.ResolveSystemAndMethodAsync` and
|
||||
`DatabaseGateway.ResolveConnectionAsync` were rewritten to call the keyed lookups; the
|
||||
`GetAllExternalSystemsAsync` / `GetMethodsByExternalSystemIdAsync` /
|
||||
`GetAllDatabaseConnectionsAsync` + in-memory `FirstOrDefault` filtering is gone from
|
||||
both hot paths.
|
||||
|
||||
Regression tests (TDD — written first, verified failing/not-compiling before the
|
||||
implementation, then confirmed green; one was additionally verified to fail when the
|
||||
keyed query is deliberately broken):
|
||||
|
||||
- ConfigurationDatabase (`RepositoryCoverageTests`):
|
||||
`GetExternalSystemByName_ReturnsMatchingRow`,
|
||||
`GetExternalSystemByName_MissingName_ReturnsNull`,
|
||||
`GetMethodByName_ReturnsMethodScopedToParentSystem`,
|
||||
`GetMethodByName_MissingMethod_ReturnsNull`,
|
||||
`GetDatabaseConnectionByName_ReturnsMatchingRow`,
|
||||
`GetDatabaseConnectionByName_MissingName_ReturnsNull`.
|
||||
- SiteRuntime (`SiteRepositoryTests`):
|
||||
`ExternalSystemRepository_GetByName_ReturnsMatchingDefinition`,
|
||||
`ExternalSystemRepository_GetByName_MissingName_ReturnsNull`,
|
||||
`ExternalSystemRepository_GetMethodByName_ResolvesScopedToSystem`,
|
||||
`DatabaseConnectionRepository_GetByName_ReturnsMatchingDefinition`.
|
||||
- ExternalSystemGateway: the existing `ExternalSystemClientTests` /
|
||||
`DatabaseGatewayTests` resolution stubs were migrated to the keyed methods (via
|
||||
`StubResolution` / `StubConnection` helpers), so the full gateway suite now exercises
|
||||
and protects the keyed-lookup resolution path.
|
||||
|
||||
`dotnet build ScadaLink.slnx` is clean; `ScadaLink.ExternalSystemGateway.Tests` (54),
|
||||
`ScadaLink.ConfigurationDatabase.Tests` (106), `ScadaLink.SiteRuntime.Tests` (196) and
|
||||
`ScadaLink.Commons.Tests` (226) all pass.
|
||||
|
||||
### ExternalSystemGateway-012 — Permanent-failure logging requirement is not met; `_logger` is injected but unused
|
||||
|
||||
|
||||
Reference in New Issue
Block a user