Skip to content

Fix admin panel hanging when the admin user storage is unreachable - #904

Merged
sven-n merged 2 commits into
masterfrom
claude/adminpanel-auth-no-refresh-1l6ayl
Aug 26, 2026
Merged

Fix admin panel hanging when the admin user storage is unreachable#904
sven-n merged 2 commits into
masterfrom
claude/adminpanel-auth-no-refresh-1l6ayl

Conversation

@sven-n

@sven-n sven-n commented Aug 26, 2026

Copy link
Copy Markdown
Member

Follow-up to #898, from user feedback: after updating, the admin panel never finished loading. The navigation stayed at Loading …, and since the setup entry lives in the branch that is hidden while loading, the installation could not even be started.

What went wrong

The authorization of every request asks whether an admin user exists, so that the panel stays reachable until the first user has been created. Both halves of that question were unbounded:

  • AdminUserRepository.EnsureStorageAsync ran Database.MigrateAsync() on every attempt, with no timeout and no backoff after a failure. The connection string allows Command Timeout=120.
  • While the first caller was stuck in that attempt, every other caller queued on the semaphore of AdminUserAvailabilityService — including the requests which render the page. The negative result was only cached after the probe returned, so nothing short-circuited while it was running.

On a system whose database was not reachable yet — right after an update, or while the installation is still running — the circuit's renders stalled behind a connection attempt that was going to time out anyway.

Fix

  • The availability check no longer waits for a probe which is already running (WaitAsync(0)); it answers with what is known instead.
  • The connection is checked separately with a short timeout (3 s) before a migration is attempted, the migration has a timeout of its own (30 s), and after a failure the storage is only probed again after a delay (30 s).
  • When the storage is unreachable, the previous answer is kept rather than being treated as "no user exists".

Testing

AvailabilityCheckDoesNotWaitForARunningProbeAsync is the actual regression test: a repository blocks its availability check on a TaskCompletionSource, and the test asserts that a concurrent check completes synchronously instead of queuing behind it. Verified that it fails against the previous implementation and passes against this one.

Two supporting tests cover that an unreachable storage is not probed on every check, and that an existing user is remembered.

My first attempt at a regression test would have passed against the broken code as well — the existing five second cache meant rapid repeated calls only probed once either way — so it is not in this change.

  • Full solution builds clean: 0 errors, no new warnings.
  • MUnique.OpenMU.Web.Tests: 29/29 pass (3 new).
  • MUnique.OpenMU.Tests: 769/769 pass.

Not verified

Still no database in the environment this was developed in, so the behaviour against a real unreachable PostgreSQL has not been exercised end to end — the tests drive the blocking through a repository double. If the panel still hangs after this, the server log is the next place to look: EnsureStorageAsync logs an Information entry with the connection exception each time it gives up.

🤖 Generated with Claude Code

https://claude.ai/code/session_015MLJ586D4tVVznbF5g7MHp


Generated by Claude Code

claude added 2 commits August 26, 2026 17:40
The authorization of every request asks whether an admin user exists, so that the
panel stays reachable until the first user was created. That question hit the
database, and both steps of it were unbounded:

- The repository ran the schema migration on every attempt, without a timeout and
  without backing off after a failure. The connection string allows a command
  timeout of two minutes.
- While the first caller was stuck in that attempt, every other caller queued up
  behind the semaphore of the availability check - including the requests which
  render the page.

So on a system whose database was not reachable yet - right after an update, or
while the installation is still running - the panel never finished loading. The
navigation stayed at "Loading ...", which also hides the setup entry, so the
installation couldn't even be started.

Now:
- The availability check never waits for a probe which is already running; it
  answers with what is known instead.
- The connection is checked separately with a short timeout before a migration is
  attempted, the migration has a timeout of its own, and after a failure the
  storage is only probed again after a delay.
- When the storage is unreachable, the previous answer is kept instead of being
  treated as "no user exists".

The regression test fails against the previous implementation.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015MLJ586D4tVVznbF5g7MHp
The containers run as a non-root user while /app belongs to root, and only
/app/logs was made writable. The data protection key ring was configured to be
stored in /app/data-protection-keys, which that user can't create - so as soon as
a key was needed, which is the case for every antiforgery token and for the
authentication cookie, the request failed and the panel showed its error page.
Mounting the volume of the compose files doesn't help either, because docker
seeds a new named volume from the image and it inherits the same ownership.

- Both images which host the admin panel create the directory and make it
  writable before they drop to the non-root user.
- The key directory is created and probed for write access when the application
  starts. If it can't be used, the panel now logs a warning naming the path and
  falls back to keys which are only kept in memory, instead of failing every
  request. That's a degradation - everybody is signed out on a restart and stored
  authenticator keys become unreadable - but it keeps the panel usable, which is
  what somebody in that situation needs in order to fix it.

The test protects a value through the configured provider with an unusable key
directory; it fails when the keys are persisted to that path unconditionally.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015MLJ586D4tVVznbF5g7MHp
@sven-n
sven-n merged commit 85dbe5f into master Aug 26, 2026
3 checks passed
@sven-n
sven-n deleted the claude/adminpanel-auth-no-refresh-1l6ayl branch August 26, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants