Skip to content

chore: cache the Lagoon token and aggregate the pre-warm pool check#229

Merged
dan2k3k4 merged 1 commit into
devfrom
chore/scheduler-queue-perf
Jul 17, 2026
Merged

chore: cache the Lagoon token and aggregate the pre-warm pool check#229
dan2k3k4 merged 1 commit into
devfrom
chore/scheduler-queue-perf

Conversation

@dan2k3k4

@dan2k3k4 dan2k3k4 commented Jul 17, 2026

Copy link
Copy Markdown
Member

What

PR 4 of 6 executing the advisory plan set (plans/, committed in #226). Implements plans 007 + 008 — the scheduler/queue throughput pair.

Changes

  • Plan 007 — Lagoon token caching: getLagoonToken() spawned an ssh subprocess per call (the only token_fetcher binding lives in one console command), so every redeploy trigger and every deployment poll paid a full SSH round-trip. Now: successful tokens cached 110s (deliberately under the FTLagoon provider stack's 2-min max token age — grilling decision), cache key derived via a public static tokenCacheKey() so the tests can't drift from the implementation. Two safety properties test-pinned: failures are never cached (one SSH blip can't poison the TTL window) and a bound token_fetcher still bypasses everything (RunLagoonCommandOnAppInstances depends on it).
  • Plan 008 — pool-check aggregation: the pre-warm poller ran up to ~3 COUNT/EXISTS queries per store app every 5 seconds. The tick now eager-loads the pool count via one grouped withCount (the accessor's preloaded-attribute short-circuit makes this free — zero accessor changes) and checks the query-free deficit condition first so idle ticks skip the EXISTS probes. Per-tick body extracted to checkOnce() for testability; dispatch truth table pinned (deficit → job on unallocated-instance-creation; satisfied pool → nothing).

Verified equivalence

The withCount filter block is byte-equivalent to getUnallocatedInstancesCountAttribute()'s own query (and to the existing withCount in PolydockStoreAppResource::getEloquentQuery()) — checked before landing, per the plan's STOP condition.

Testing

  • php artisan test — 438 passed (5 new)
  • ./vendor/bin/phpstan analyse — no errors; Pint clean

Greptile Summary

This PR implements two query-reduction optimizations: a 110-second token cache inside getLagoonToken() that avoids an SSH subprocess on every redeploy/poll call, and an aggregated withCount eager-load in the pre-warm poller that replaces per-app COUNT queries with a single grouped query per tick.

  • Plan 007 wraps fetchLagoonTokenOverSsh() with a Cache::get/put layer keyed on SSH credentials and endpoint; failures are never cached to avoid a transient SSH error poisoning the TTL window; a bound token_fetcher short-circuits the whole path unchanged.
  • Plan 008 extracts checkOnce() from the polling loop, adds withCount(['instances as unallocated_instances_count' => ...]) so the model accessor's preloaded-attribute short-circuit eliminates the per-app COUNT queries on deficit ticks, and the EXISTS probes only fire for over-target or refresh-configured pools.
  • Five new tests pin the token cache invariants (hit, miss-not-cached, fetcher bypass) and the dispatch truth table (deficit → job dispatched; satisfied pool → nothing).

Confidence Score: 5/5

Safe to merge — both optimizations are additive and preserve all existing caller contracts.

The token cache correctly avoids storing failures, the bound-fetcher bypass path is unchanged, and the withCount alias exactly matches the model accessor fallback query so the short-circuit is transparent. No contracts are broken, no data is mutated differently, and the five new tests cover the key invariants.

No files require special attention.

Important Files Changed

Filename Overview
app/Services/LagoonClientService.php Token caching layer added; tokenCacheKey() correctly includes all five config dimensions; failures correctly bypass Cache::put; bound fetcher short-circuit preserved.
app/Console/Commands/PollEnsureUnallocatedAppInstancesJobCommand.php Poll loop refactored to call checkOnce(); withCount filter is byte-equivalent to the model accessor fallback query; public checkOnce() correctly returns maintenance count for testability.
tests/Feature/Services/LagoonClientServiceTokenCacheTest.php Three new tests pin cache-hit, failure-not-cached, and fetcher-bypass invariants; setUp uses offsetUnset instead of forgetInstance; Cache::flush ensures test isolation.
tests/Feature/Console/PollUnallocatedInstancesCheckTest.php Two new tests cover the dispatch truth table for deficit and satisfied-pool conditions via checkOnce(); factory defaults produce supports_pre_warming=true so the deficit path exercises correctly.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant LCS as LagoonClientService
    participant Cache
    participant SSH as fetchLagoonTokenOverSsh

    Caller->>LCS: getLagoonToken(config)
    alt bound token_fetcher registered
        LCS-->>Caller: app('polydock.lagoon.token_fetcher')(config)
    else no fetcher bound
        LCS->>Cache: get(tokenCacheKey(config))
        alt cache hit (non-empty string)
            Cache-->>LCS: cached token
            LCS-->>Caller: cached token
        else cache miss or empty
            Cache-->>LCS: null / ''
            LCS->>SSH: fetchLagoonTokenOverSsh(config)
            SSH-->>LCS: token or ''
            alt "token !== ''"
                LCS->>Cache: put(key, token, +110s)
            end
            LCS-->>Caller: token (may be '')
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant LCS as LagoonClientService
    participant Cache
    participant SSH as fetchLagoonTokenOverSsh

    Caller->>LCS: getLagoonToken(config)
    alt bound token_fetcher registered
        LCS-->>Caller: app('polydock.lagoon.token_fetcher')(config)
    else no fetcher bound
        LCS->>Cache: get(tokenCacheKey(config))
        alt cache hit (non-empty string)
            Cache-->>LCS: cached token
            LCS-->>Caller: cached token
        else cache miss or empty
            Cache-->>LCS: null / ''
            LCS->>SSH: fetchLagoonTokenOverSsh(config)
            SSH-->>LCS: token or ''
            alt "token !== ''"
                LCS->>Cache: put(key, token, +110s)
            end
            LCS-->>Caller: token (may be '')
        end
    end
Loading

Reviews (2): Last reviewed commit: "chore: cache the Lagoon token and aggreg..." | Re-trigger Greptile

Comment thread app/Console/Commands/PollEnsureUnallocatedAppInstancesJobCommand.php Outdated
Comment thread app/Services/LagoonClientService.php
Comment thread tests/Feature/Services/LagoonClientServiceTokenCacheTest.php Outdated
Implements plans 007 and 008 from the advisory audit:

- LagoonClientService minted a token over SSH (process spawn, 30s
  timeout) on every call unless a fetcher was bound — the deployment
  service pays that per redeploy trigger and per poll, so rollout bursts
  serialized workers behind repeated SSH auth. Successful tokens are now
  cached for 110 seconds (under the FTLagoon provider stack's 2-minute
  max token age), keyed on user/server/port/keyfile via a public static
  tokenCacheKey() so tests and implementation cannot drift. Failures ('')
  are never cached — one SSH blip must not poison callers for the TTL —
  and a bound token_fetcher still short-circuits everything.
- The pre-warm poller evaluated needs_unallocated_maintenance per store
  app every 5 seconds — up to ~3N COUNT/EXISTS queries per tick, scaling
  with catalog size regardless of change. The tick now eager-loads the
  pool count in one grouped withCount (the accessor short-circuits on the
  preloaded attribute, unchanged) and checks the query-free deficit
  condition first, so idle ticks skip the EXISTS probes entirely. The
  per-tick body moved to checkOnce() so tests don't fight the sleep loop;
  the truth table (deficit dispatches / satisfied pool doesn't) is pinned.
@dan2k3k4
dan2k3k4 force-pushed the chore/scheduler-queue-perf branch from e7fd08f to a5f8982 Compare July 17, 2026 15:35
@dan2k3k4
dan2k3k4 merged commit 6cf3b6d into dev Jul 17, 2026
5 checks passed
@dan2k3k4
dan2k3k4 deleted the chore/scheduler-queue-perf branch July 17, 2026 16:19
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.

1 participant