| | | 1 | | using ArturRios.Heimdall.Data.Configuration; |
| | | 2 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 3 | | using ArturRios.Heimdall.Domain.Persistence; |
| | | 4 | | using Microsoft.EntityFrameworkCore; |
| | | 5 | | |
| | | 6 | | namespace 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> |
| | 72 | 35 | | public 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 | | { |
| | 48 | 45 | | if (context.Database.CurrentTransaction is null) |
| | | 46 | | { |
| | 0 | 47 | | throw new InvalidOperationException( |
| | 0 | 48 | | "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. |
| | 48 | 53 | | 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. |
| | 31 | 57 | | var scopeTable = context.Model.FindEntityType(typeof(Scope))!.GetSchemaQualifiedTableName(); |
| | 31 | 58 | | var ids = scopeIds.Distinct().Order().ToArray(); |
| | | 59 | | |
| | 31 | 60 | | var sql = $"SELECT 1 FROM {scopeTable} WHERE id = ANY({{0}}) ORDER BY id FOR NO KEY UPDATE"; |
| | 31 | 61 | | await context.Database.ExecuteSqlRawAsync(sql, ids); |
| | | 62 | | } |
| | | 63 | | |
| | 48 | 64 | | if (systemAdmins) |
| | | 65 | | { |
| | 26 | 66 | | await context.Database.ExecuteSqlRawAsync("SELECT pg_advisory_xact_lock({0})", SystemAdminSetKey); |
| | | 67 | | } |
| | 48 | 68 | | } |
| | | 69 | | } |