< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Data.Persistence.InvariantLocks
Assembly: ArturRios.Heimdall.Data
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/InvariantLocks.cs
Line coverage
83%
Covered lines: 10
Uncovered lines: 2
Coverable lines: 12
Total lines: 69
Line coverage: 83.3%
Branch coverage
83%
Covered branches: 5
Total branches: 6
Branch coverage: 83.3%
Method coverage
100%
Covered methods: 2
Fully covered methods: 1
Total methods: 2
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
LockAsync()83.33%6681.81%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/InvariantLocks.cs

#LineLine coverage
 1using ArturRios.Heimdall.Data.Configuration;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Domain.Persistence;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace ArturRios.Heimdall.Data.Persistence;
 7
 8/// <summary>
 9///     <see cref="IInvariantLocks" /> on PostgreSQL: <c>SELECT 1 FROM heimdall.scope WHERE id = ANY(…)
 10///     ORDER BY id FOR NO KEY UPDATE</c> for the scopes, then <c>pg_advisory_xact_lock</c> on
 11///     <see cref="SystemAdminSetKey" /> for the System Admin set, both released when the transaction ends.
 12/// </summary>
 13/// <remarks>
 14///     <para>
 15///         The scope rows are the natural thing to serialise NFR-12 on: every owner change names its
 16///         scope, and <see cref="ApplicationCredentialSlots" /> already serialises on its parent row the
 17///         same way. The System Admin set has no row of its own, so it gets an advisory lock: one key for
 18///         the whole set, since the rule is about the set (#203).
 19///     </para>
 20///     <para>
 21///         <b>Why <c>FOR NO KEY UPDATE</c>, not <c>FOR UPDATE</c>.</b> It conflicts with itself, so the
 22///         invariant writers serialise on a scope exactly as they would under <c>FOR UPDATE</c>, and with the
 23///         scope's own update and delete (UC-04, UC-05). It does not conflict with the <c>FOR KEY SHARE</c> an
 24///         insert of a row referencing the scope takes — an owner, a member, an application, a pending
 25///         registration — so those inserts are not held up by a scope's invariant writers, and no cycle can form
 26///         between such an insert, a writer holding the scope and a writer waiting on the inserted row.
 27///     </para>
 28///     <para>
 29///         The runtime login (NFR-29) holds <c>UPDATE</c> on <c>heimdall.scope</c>, which is what
 30///         <c>FOR NO KEY UPDATE</c> needs, and may call <c>pg_advisory_xact_lock</c>: PUBLIC keeps the
 31///         <c>EXECUTE</c> it has on <c>pg_catalog</c>'s functions, which <c>runtime-grants.sql</c> never
 32///         revokes. So neither lock needs a grant the login lacks.
 33///     </para>
 34/// </remarks>
 7235public sealed class InvariantLocks(AppDbContext context) : IInvariantLocks
 36{
 37    /// <summary>
 38    ///     The advisory lock key of the System Admin set (NFR-32): <c>"HEIMSYSA"</c> in ASCII, so it is
 39    ///     recognisable in <c>pg_locks</c>. Nothing else takes an advisory lock in this database.
 40    /// </summary>
 41    public const long SystemAdminSetKey = 0x4845494D53595341;
 42
 43    public async Task LockAsync(IReadOnlyCollection<long> scopeIds, bool systemAdmins = false)
 44    {
 4845        if (context.Database.CurrentTransaction is null)
 46        {
 047            throw new InvalidOperationException(
 048                "An invariant lock is taken inside the command's transaction; outside one it ends with its statement.");
 49        }
 50
 51        // The order IInvariantLocks documents: the scope rows, in id order and in one statement, and then
 52        // the System Admin set.
 4853        if (scopeIds.Count > 0)
 54        {
 55            // The table name comes from the model, never from input, so building it into the SQL is safe;
 56            // the ids stay a parameter.
 3157            var scopeTable = context.Model.FindEntityType(typeof(Scope))!.GetSchemaQualifiedTableName();
 3158            var ids = scopeIds.Distinct().Order().ToArray();
 59
 3160            var sql = $"SELECT 1 FROM {scopeTable} WHERE id = ANY({{0}}) ORDER BY id FOR NO KEY UPDATE";
 3161            await context.Database.ExecuteSqlRawAsync(sql, ids);
 62        }
 63
 4864        if (systemAdmins)
 65        {
 2666            await context.Database.ExecuteSqlRawAsync("SELECT pg_advisory_xact_lock({0})", SystemAdminSetKey);
 67        }
 4868    }
 69}