Skip to content

fix(ui): keep GUI auth status in sync via periodic polling - #1239

Draft
andychoquette wants to merge 1 commit into
aws-deadline:mainlinefrom
andychoquette:chore/user-not-shown-as-signed-out
Draft

fix(ui): keep GUI auth status in sync via periodic polling#1239
andychoquette wants to merge 1 commit into
aws-deadline:mainlinefrom
andychoquette:chore/user-not-shown-as-signed-out

Conversation

@andychoquette

Copy link
Copy Markdown
Contributor

What was the problem/requirement? (What/Why)

When the GUI is left open and the Deadline authentication status changes to a non-authenticated state — credentials expiring in place, or being changed out-of-process (e.g. a deadline auth logout from a terminal) — the GUI did not update. It kept showing "Authenticated" indefinitely.

The GUI only re-checked authentication status reactively:

  • on construction of DeadlineAuthenticationStatus,
  • on QFileSystemWatcher events for ~/.aws and ~/.deadline,
  • on explicit login/logout from a dialog.

None of these fire for in-place credential expiry or out-of-process changes. In particular, deadline auth logout delegates to Deadline Cloud Monitor, which invalidates a token cache without necessarily modifying a watched file, so the file watcher does not reliably detect it.

What was the solution? (How)

Added a QTimer to DeadlineAuthenticationStatus that re-probes authentication status every 30s as a backstop, independent of the file watcher. To keep this from causing visible churn:

  • A new quiet-refresh mode (refresh_status(quiet=True)) skips the reset-to-None so widgets don't flash "Refreshing" on every tick.
  • Status/availability change signals are emitted only when a value actually differs from the cached one, so a steady authenticated state produces no UI updates.
  • A poll tick is skipped while a refresh is already in flight.

All existing auth-status widgets (config/settings dialog, submit dialog) pick this up through the signals they already connect to — no widget changes needed.

What is the impact of this change?

While a GUI is open, the auth-status widgets reflect the true state within ~30s of a change, and the submit button enables/disables accordingly. Cost is a low-frequency deadline:ListFarms probe every 30s while a GUI is open. No public API or CLI contract changes.

How was this change tested?

  • Added unit tests in test/unit/deadline_client/ui/gui/test_deadline_authentication_status.py covering timer setup/interval, poll runs a quiet refresh, poll skipped when a refresh is in flight, quiet refresh preserves cached values, loud refresh clears them, no signal when status is unchanged, and signals fire on the AUTHENTICATED → NEEDS_LOGIN transition.

  • Ran the new tests plus related dialog/auth GUI suites (test_settings_dialogue.py, test_submit_job_to_deadline_dialog.py) — all pass.

  • Manually verified in the GUI: logged in, opened the GUI, ran deadline auth logout from a separate terminal, and confirmed the widget flipped to the "Log in" prompt within the poll interval without interacting with the GUI.

  • Have you run the unit tests? Yes.

  • Have you run the integration tests? No (change is GUI/auth-status logic, covered by unit tests).

Was this change documented?

  • Are relevant docstrings in the code base updated? Yes — refresh_status documents the new quiet parameter, and the new timer/quiet-refresh methods are documented.
  • Has the README.md been updated? Not applicable — no CLI arguments or user-facing options changed.

Does this PR introduce new dependencies?

  • This PR adds one or more new dependency Python packages. I acknowledge I have reviewed the considerations for adding dependencies in DEVELOPMENT.md.
  • This PR does not add any new dependencies.

Is this a breaking change?

No. refresh_status gains an optional quiet parameter that defaults to the prior behavior, so existing callers are unaffected.

Does this change impact security?

No. This change does not create or modify files/directories, and does not change credential handling — it only re-runs the existing authentication-status probe on a timer.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions github-actions Bot added the waiting-on-maintainers Waiting on the maintainers to review. label Jun 30, 2026
"""
if self._runner.is_running("auth_status") or self._runner.is_running("creds_source"):
return
self.refresh_status(quiet=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The poll runs refresh_status(quiet=True), which calls the probes (get_credentials_source / check_authentication_status) through get_boto3_clientget_boto3_session(config=...) without force_refresh=True. Those resolve to the @lru_cached session (_get_boto3_session_for_profile).

This is deliberately different from the files_changed handler, which force-refreshes the session (_get_session_backgroundget_boto3_session(force_refresh=True)) precisely so out-of-process credential rewrites are picked up.

The motivating comment (lines 41-45 / 115-118) says the poll exists to catch credentials "changed out-of-process" and in-place rewrites the watcher misses. But for static credentials (HOST_PROVIDED access keys / session tokens in ~/.aws/credentials), boto3 reads and caches them in the session object on first use. An in-place rewrite — a previously-invalid creds file being repaired, or new keys swapped in — won't be re-read by the stale cached session, so the poll keeps reporting the old state until something else force-refreshes.

Pure expiry of an existing token is still detected (the API call rejects it), and DCM credential_process profiles self-refresh, so those cases work. The gap is specifically in-place changes to static credentials — part of what the poll is documented to cover. Consider force-refreshing the session in the quiet poll path so the backstop actually reflects on-disk changes.

@andychoquette
andychoquette force-pushed the chore/user-not-shown-as-signed-out branch from e1ef41e to 7cb8fa7 Compare June 30, 2026 17:54
"""
if self._runner.is_running("auth_status") or self._runner.is_running("creds_source"):
return
self._refresh_status(quiet=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The poll re-probes with the cached boto3 session — it does not force_refresh, unlike the file-watcher path.

_poll_auth_status_refresh_status(quiet=True) runs check_authentication_status/get_credentials_source, both of which call get_boto3_session() without force_refresh=True. That returns the lru_cached session (_get_boto3_session_for_profile), whose credentials are resolved/held in memory. By contrast, the files_changed path deliberately calls api.get_boto3_session(force_refresh=True) before refreshing.

Consequence for the scenarios this backstop is meant to cover (per the comment on lines 41-44 / 115-118):

  • Credential expiry of refreshable (DCM custom-process) creds is still caught, because botocore re-runs the credential process when the cached creds near expiry and a failure surfaces as NEEDS_LOGIN. ✅
  • Out-of-process changes to static host-provided credentials (e.g. an in-place rewrite of ~/.aws/credentials, or a logout that the QFileSystemWatcher misses) are not picked up: the cached session keeps serving the stale in-memory creds, so the poll re-confirms the old state. ✗

If the intent is for the poll to reflect out-of-process credential changes that the watcher misses (which is the stated rationale), it likely needs to force a session-cache invalidation (e.g. get_boto3_session(force_refresh=True)) before re-probing.

@andychoquette
andychoquette force-pushed the chore/user-not-shown-as-signed-out branch from 7cb8fa7 to fb2fecf Compare June 30, 2026 21:03
@andychoquette
andychoquette force-pushed the chore/user-not-shown-as-signed-out branch 2 times, most recently from aefb0f2 to 8be049d Compare July 2, 2026 14:56
Comment thread test/conftest.py

status = auth_module._deadline_authentication_status
if status is not None:
timer = getattr(status, "_poll_timer", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new autouse fixture runs for every test in the suite and, in _stop_auth_status_poll_timer(), does import deadline.client.ui.deadline_authentication_status. That module imports from qtpy.QtCore import ... at module top, which raises when no Qt binding (PySide6) is installed.

The GUI test conftest (test/unit/deadline_client/ui/gui/conftest.py) deliberately supports running the suite without PySide6 by collect_ignore-ing the Qt test files when importlib.util.find_spec("PySide6") is None. This root autouse fixture bypasses that guard: with the gui extra absent, the fixture setup would raise ImportError/QtBindingsNotFoundError and turn every test in the suite into an error, not just the GUI ones.

In practice the hatch test envs all set features = ["gui"], so PySide6 is always present in CI and this stays latent — but it makes the whole suite hard-depend on the optional GUI binding, contrary to the no-PySide6 path the gui conftest goes out of its way to preserve. Consider guarding the import (e.g. skip when importlib.util.find_spec("PySide6") is None, or wrap the import in a try/except ImportError).

@andychoquette
andychoquette force-pushed the chore/user-not-shown-as-signed-out branch 2 times, most recently from 65f2d0b to c27c3e2 Compare July 2, 2026 15:44
"""
if self._runner.is_running("auth_status") or self._runner.is_running("creds_source"):
return
self._refresh_status(quiet=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling turns any transient probe failure into a spurious UI transition every 30s.

_poll_auth_status_refresh_status(quiet=True)check_authentication_status, which returns CONFIGURATION_ERROR (or NEEDS_LOGIN for a monitor profile) on any exception from the deadline:ListFarms probe — including transient network errors, timeouts, and throttling (_session.py:424). There is no retry or debounce.

Because _set_auth_status now emits only on change, a single transient failure while a user is steadily AUTHENTICATED will flip the UI to "Configuration error" / "Log in", then flip back on the next successful poll — a recurring flicker for anyone on a flaky network. Before this change the status was recomputed only on user-driven/file-change events, so this continuous exposure is new.

Consider only downgrading the cached AUTHENTICATED state on a definitive auth failure (e.g. distinguish credential/expiry errors from transient service/network errors), or requiring N consecutive quiet failures before emitting a regression, so a momentary blip does not repeatedly toggle the UI.

@andychoquette
andychoquette force-pushed the chore/user-not-shown-as-signed-out branch 3 times, most recently from f200169 to 26fe6be Compare July 2, 2026 16:59
The GUI only re-checked authentication status reactively: on construction,
on QFileSystemWatcher events for ~/.aws and ~/.deadline, and on explicit
login/logout from a dialog. None of those fire when credentials expire in
place or are changed out-of-process (e.g. a `deadline auth logout` from a
terminal), so an open GUI kept showing "Authenticated" indefinitely.

Add a QTimer to DeadlineAuthenticationStatus that re-probes auth status
every 30s as a backstop, independent of the file watcher. The poll uses a
quiet refresh that skips the reset-to-None (so widgets don't flash
"Refreshing" each tick) and emits change signals only when a value actually
differs, so a steady authenticated state produces no UI churn. A tick is
skipped while a refresh is already in flight.

The quiet poll force-refreshes the boto3 session before probing, so
out-of-process credential changes the file watcher misses (an in-place
rewrite of ~/.aws/credentials, or a logout) are re-read rather than
validated against the stale cached session. To avoid a transient network or
throttling blip flipping the UI to "Log in"/error and back every interval,
a previously-AUTHENTICATED state is only downgraded after N consecutive
quiet-poll failures; user-driven refreshes still surface errors immediately.

Polling is ref-counted and scoped to the lifetime of a visible auth-status
widget (started in the widget's constructor, released on its `destroyed`
signal), so no background AWS probes run in a headless process.

Add a root autouse test fixture that stops the poll timer before and after
every test (guarded so it's a no-op when the optional GUI extra / PySide6 is
absent), so the process-wide singleton's timer can't run alongside an
unrelated test under pytest-xdist.

Signed-off-by: Andy Choquette <78888816+andychoquette@users.noreply.github.com>
@andychoquette
andychoquette force-pushed the chore/user-not-shown-as-signed-out branch from 26fe6be to 6165019 Compare July 6, 2026 21:08
# after N consecutive failures, so a momentary blip does not flip the
# widget to "Log in"/"error" and back every poll interval. User-driven
# refreshes (quiet=False) still surface the error immediately.
if quiet and self.__auth_status == api.AwsAuthenticationStatus.AUTHENTICATED:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transient-failure debounce added here is placed on the wrong handler, so it does not actually prevent the flicker it targets.

check_authentication_status (_session.py:420-430) catches every probe exception internally and returns AUTHENTICATED / NEEDS_LOGIN / CONFIGURATION_ERROR — it does not raise. So a transient network/throttling blip during the deadline:ListFarms probe resolves the background task successfully (with a CONFIGURATION_ERROR/NEEDS_LOGIN value) and flows through _on_auth_status_success, not _on_auth_status_error.

_on_auth_status_success unconditionally resets self.__consecutive_poll_failures = 0 and calls _set_auth_status(result, ...), which emits on change → the UI still flips AUTHENTICATED → "Log in"/"error" on a single transient blip and back on the next poll. The __consecutive_poll_failures counter here in _on_auth_status_error only ever increments if _refresh_auth_status itself raises (e.g. the get_boto3_session(force_refresh=True) call throws) — not for the ordinary probe-failure path the debounce is documented to cover.

To actually debounce transient probe failures, the consecutive-failure gating needs to run in the success handler based on the returned status (i.e. treat a quiet transition away from AUTHENTICATED as the thing to debounce), not only when the task errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on-maintainers Waiting on the maintainers to review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants