< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Email.PasswordRecoveryQueue
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Email/PasswordRecoveryQueue.cs
Line coverage
100%
Covered lines: 15
Uncovered lines: 0
Coverable lines: 15
Total lines: 73
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage
100%
Covered methods: 3
Fully covered methods: 3
Total methods: 3
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Reader()100%11100%
TryEnqueue(...)100%22100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Email/PasswordRecoveryQueue.cs

#LineLine coverage
 1using System.Threading.Channels;
 2using ArturRios.Heimdall.Command.Services;
 3
 4namespace ArturRios.Heimdall.WebApi.Email;
 5
 6/// <summary>
 7///     The in-process queue between UC-12's handler and <see cref="PasswordRecoveryDispatcher" />.
 8///     Bounded, never blocks the caller, and holds only what the caller sent: an address and an
 9///     optional scope.
 10/// </summary>
 11/// <remarks>
 12///     <para>
 13///         <b>Why a queue, and why in memory.</b> AF-12a's anti-enumeration promise needs the request
 14///         path to do the same work for every address. Two shapes were considered:
 15///     </para>
 16///     <list type="bullet">
 17///         <item>
 18///             A constant response-time floor. To hide a Mailgun round trip it would have to exceed
 19///             Mailgun's worst latency — the client's timeout, a hundred seconds — or leak exactly on
 20///             the slow sends, and it would hold every caller's connection open for that long, which
 21///             turns the endpoint into a cheap way to tie up the server.
 22///         </item>
 23///         <item>
 24///             A durable outbox in the database. It survives a restart, but it would keep every
 25///             address anybody typed — registered or not — at rest, which is personal data of people
 26///             who are not users, with no retention rule covering it. And nothing here is worth that:
 27///             a lost request is one the caller simply repeats, since UC-12 promises nothing more
 28///             than "if the address is registered, a link is on its way".
 29///         </item>
 30///     </list>
 31///     <para>
 32///         The queue holds no secret: the reset token is created by the dispatcher, after the
 33///         request has left. It lives as long as the process does, and a restart drops whatever was
 34///         waiting — at most a few seconds' worth, given the per-address rate limit in front of it.
 35///     </para>
 36///     <para>
 37///         <b>Bounded, and dropping when full.</b> <see cref="Capacity" /> caps the memory a burst can
 38///         take while Mailgun is slow or down. A request that does not fit is dropped and logged
 39///         rather than waited for, because waiting would put Mailgun's latency back on the request
 40///         path — the thing the queue exists to remove.
 41///     </para>
 42/// </remarks>
 90243public sealed class PasswordRecoveryQueue(ILogger<PasswordRecoveryQueue> logger) : IPasswordRecoveryQueue
 44{
 45    /// <summary>Requests held at most while the dispatcher catches up.</summary>
 46    public const int Capacity = 1024;
 47
 90248    private readonly Channel<PasswordRecoveryRequest> _channel = Channel.CreateBounded<PasswordRecoveryRequest>(
 90249        new BoundedChannelOptions(Capacity)
 90250        {
 90251            // Wait, not one of the Drop modes: TryWrite never waits in any mode, and Wait is the one
 90252            // where a full queue makes TryWrite report false instead of silently discarding an item.
 90253            FullMode = BoundedChannelFullMode.Wait,
 90254            SingleReader = true
 90255        });
 56
 57    /// <summary>The dispatcher's end of the queue.</summary>
 90158    public ChannelReader<PasswordRecoveryRequest> Reader => _channel.Reader;
 59
 60    public bool TryEnqueue(PasswordRecoveryRequest request)
 61    {
 105662        if (_channel.Writer.TryWrite(request))
 63        {
 105564            return true;
 65        }
 66
 67        // No address in the log (NFR-22): which request was dropped does not matter, that one was.
 168        logger.LogWarning(
 169            "The password recovery queue is full ({Capacity}); a recovery request was dropped", Capacity);
 70
 171        return false;
 72    }
 73}