Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

Chapter 10 — Watching it run

Goal: be able to answer "is the background work actually happening?" without SSH-ing into anything. You'll add: an /ops page that reads every pillar's own table.

Shop now has five background workers: the outbox relay, the job processor, the mail sender, the cache purger and the snapshot service. All of them succeed silently and fail silently. Before you deploy, you want one page that says what they're doing.

Here is where the DB-backed design pays off a second time. There is no broker console to open and no separate metrics stack to stand up, because every pillar's state is a table in the database you already query. Queue depth is SELECT count(*).

1. The tables

Pillar Table The question it answers
Rask.Outbox OutboxMessage How many events are waiting, delivered, or erroring?
Rask.Jobs Job, RecurringJobState Is anything queued, and did the recurring work fire?
Rask.Mail QueuedMail Is mail going out, or piling up?
Rask.Cache CacheEntry How much is cached right now?

They're ordinary EF entities, mapped into the same RaskAppDbContext as your products. They aren't aggregates, so there's no OutboxMessage.Where(…) — you reach them through the context itself. Inject IDbContextFactory<RaskAppDbContext> into the page as dbFactory and open one per refresh:

await using var db = await dbFactory.CreateDbContextAsync(ct);

2. Count what matters

Three counts tell you almost everything:

OutboxProcessed = await db.Set<OutboxMessage>().CountAsync(m => m.ProcessedAt != null, ct),
OutboxFailed    = await db.Set<OutboxMessage>().CountAsync(m => m.Error != null, ct),
JobsProcessed   = await db.Set<Job>().CountAsync(j => j.ProcessedAt != null, ct),

The failed count is the one people leave out, and it's the one that matters. Delivery is at-least-once with retries: a message the processor can't handle doesn't throw and doesn't disappear — it records an Error, increments Attempts, and comes back on the next poll until it hits MaxAttempts. A dashboard that only shows "processed" reports a healthy system while an event retries itself to death.

So: processed climbing is good, failed above zero is the alert.

3. Make it live

A page that needs refreshing won't get looked at. Poll in the background and re-render on a real change:

protected override async Task OnMount()
{
    await Refresh().ConfigureAwait(false);
    _ = Poll();
}

private async Task Poll()
{
    for (var tick = 0; tick < MaxTicks && !_stopped.IsCancellationRequested; tick++)
    {
        await Task.Delay(1.Second, _stopped.Token).ConfigureAwait(false);

        var before = _stats;
        await Refresh().ConfigureAwait(false);
        if (_stats != before)
        {
            StateHasChanged();
        }
    }
}

Three things in that loop are deliberate:

  • ConfigureAwait(false) on every await. It keeps the loop off the lifecycle sync-context, so the framework doesn't render once per await. You call StateHasChanged() yourself, once per real change.
  • Compare before re-rendering. _stats is a readonly record struct, so != is a value comparison. An idle system produces no diffs and no WebSocket traffic.
  • The loop is bounded. Every open tab is a reader competing with five writers for SQLite's single write lock. An unbounded 1 Hz poll per session is a real cost — stop after a few minutes.

4. Prove the pragmas

While you're here, read the connection settings back from the live database, rather than trusting that the database was opened the way Chapter 8 said:

JournalMode = await ScalarAsync(db, "PRAGMA journal_mode"),   // → wal
ForeignKeys = await ScalarAsync(db, "PRAGMA foreign_keys"),   // → 1

Verify

  • Buy a product and watch, without touching anything: outbox processed goes up, then jobs processed, then mail sent — the chain from Chapter 7 moving through three pillars.
  • Outbox failed stays at 0.
  • journal_mode reads wal and foreign_keys reads 1.

Beyond counters. RaskApp also answers /health, including whether the live-session pool has room, and rask deploy probes it to gate a zero-downtime swap. Standard ILogger output goes wherever you point it. See observability.

5. You don't have to write this one

You just did, and it was worth doing — the point of the exercise is that every pillar's state is an ordinary table you can SELECT from, and now you've proved it.

For the parts you'd rather not hand-roll, Rask.Dashboard ships the same idea finished: /_rask with the queue rows behind each counter, the error that caused each dead letter, a one-click retry, a log tail, and the pragmas you read above. It is a battery like the others, so your Shop already has it — nothing in Program.cs, and app.Configure(c => c.Ops.Off()) (or --no-ops) leaves it out.

The dashboard shows job payloads, stored email bodies and log lines, so it is gated on the admin role — the one the first account to register holds — rather than on merely being signed in, which on an app with open registration would mean everyone. Sign in as that first account and visit https://localhost:5001/_rask to compare it with the page you just built.

Learn more: dashboard · observability · jobs · outbox

Next → Chapter 11: Deploy to one box

Confirm