Skip to content

[Task] Host Health Execution, Caching, and Scoped Dependencies #17

Description

@rian-be

Summary

Extend the AuthKit host health infrastructure to execute plugin health checks safely and efficiently, and to cache their results for a configurable TTL.

The host must create dedicated dependency injection scope for each health check execution and measure health check execution latency. Latency is host observed execution metadata and must not be placed inside PluginHealthResult.Data, which stays plugin owned diagnostic metadata (see D5–D6). To keep the plugin contract clean, the host wraps the plugin result together with the measured duration in a host-side execution result, and caches that wrapper.

Goal

Provide production safe host side execution of the structured plugin health contract introduced in D1–D6.

The implementation must ensure that:

  • plugin health checks execute within scoped IServiceProvider,
  • scoped dependencies are disposed correctly (including async disposal),
  • health check execution latency is measured by the host,
  • the measured latency is exposed through a host-side wrapper, never inside plugin Data,
  • repeated health requests do not unnecessarily execute expensive plugin checks,
  • cached results expire according to a defined TTL,
  • and cancellation is propagated correctly.

Background

Plugin health checks may depend on scoped services such as:

Database contexts
Repositories
External service clients
Transactions
Other scoped dependencies

Resolving these services directly from the root IServiceProvider can cause lifetime violations, dependency resolution errors, or resource leaks.

Health endpoints may also be polled frequently by:

Load balancers
Kubernetes probes
Monitoring systems
Reverse proxies
Service discovery

Executing every plugin health check on every request can therefore generate unnecessary database, network, and other dependency traffic.

The host should cache completed health executions for configurable TTL while still allowing health information to expire and refresh.

Finally, health check latency should be measured by the host rather than requiring every plugin to implement its own timing logic.

Scope

D7. Host Side Health Check Execution

The host must execute each plugin health check inside dedicated DI scope and measure its execution duration.

To keep the plugin contract unchanged, the host introduces host side result type that wraps the plugin's PluginHealthResult together with the measured duration:

public sealed record PluginHealthExecutionResult(
    IReadOnlyCollection<PluginHealthResult> Results,
    TimeSpan Duration);

D7 covers:

  1. create the DI scope,
  2. resolve the scoped dependencies required by the health check,
  3. execute the health check (honoring CancellationToken from D4),
  4. measure the execution duration (host-side),
  5. dispose the scope (including async disposal),
  6. build PluginHealthExecutionResult(Results, Duration),
  7. propagate exceptions and cancellation explicitly.

PluginHealthResult.Data remains plugin owned diagnostic metadata. Host latency is never written into Data.

D8. Health Result Caching

The host must cache completed health executions for configurable TTL.

The cache stores the host side execution wrapper together with its expiration, not bare PluginHealthResult:

internal sealed record CachedPluginHealthExecution(
    IReadOnlyCollection<PluginHealthResult> Results,
    TimeSpan Duration,
    DateTimeOffset ExpiresAt);

D8 covers:

  1. cache the completed PluginHealthExecutionResult as CachedPluginHealthExecution,
  2. associate the cache entry with the correct plugin/health context (cache keys),
  3. expire entries according to the TTL,
  4. support explicit cache disabling,
  5. de-duplicate concurrent refreshes (single flight),
  6. keep concurrent access thread safe,
  7. never cache an incomplete/cancelled execution as valid result.

The implementation must not resolve scoped plugin dependencies directly from the root service provider.

Health Check Execution

The expected execution model is:

Health Request
      │
      ▼
Check Health Cache
      │
      ├── Valid cached CachedPluginHealthExecution
      │       │
      │       ▼
      │    Return Results
      │
      └── Cache miss / expired
              │
              ▼
        Create (Async)Scope
              │
              ▼
       Resolve dependencies
              │
              ▼
      Execute plugin check
              │
              ▼
       Measure latency (Duration)
              │
              ▼
       Build PluginHealthExecutionResult(Results, Duration)
              │
              ▼
       Dispose scope
              │
              ▼
       Cache CachedPluginHealthExecution
              │
              ▼
         Return Results

Requirements

Scoped IServiceProvider

Each uncached plugin health check execution must occur inside dedicated DI scope.

The host should use the equivalent of:

await using var scope = serviceProvider.CreateAsyncScope();

before resolving scoped dependencies required by the health check, because the health execution pipeline is asynchronous and scoped services may implement IAsyncDisposable.

If the current AuthKit host architecture requires synchronous disposal, the host may use:

using var scope = serviceProvider.CreateScope();

The scoped provider must be used for dependency resolution.

The host must not resolve plugin scoped dependencies from the root:

serviceProvider

when scoped provider is required. The scope must be disposed after health check execution completes.

Scope Lifetime

A health check scope must remain alive for the complete execution of the health check.

The scope must be disposed after:

  • successful execution,
  • failed execution,
  • cancellation,
  • or another terminal execution path.

The implementation must not leak scoped services.

Async Scope

Because health execution is asynchronous (health checks are Task/ValueTask), prefer:

await using var scope = serviceProvider.CreateAsyncScope();

This ensures scoped services implementing IAsyncDisposable are disposed correctly. The selected mechanism must remain compatible with the target framework and existing AuthKit host conventions.

Dependency Resolution

The host must ensure that dependencies required by the plugin health check are resolved from the newly created scope.

A scoped dependency must not be captured and reused across independent health check executions. Each fresh health check execution must receive an appropriate scope.

Latency Measurement

The host must measure the duration of plugin health check execution.

The measurement should use a monotonic timing mechanism suitable for elapsed duration measurement, such as:

Stopwatch

The host must not calculate latency using wall clock timestamps where clock adjustments could produce incorrect elapsed durations.

The measured duration is represented as TimeSpan Duration on PluginHealthExecutionResult (and propagated to CachedPluginHealthExecution). The host may also expose it as latency_ms in serialized host output, but the source of truth is the host-side Duration, never PluginHealthResult.Data.

Latency Ownership

Latency is host observed execution metadata.

Plugins must not be required to calculate latency themselves. PluginHealthResult.Data is plugin-owned diagnostic metadata (D5–D6): connection counts, endpoint URLs, custom plugin timings, etc.

Because the host stores its measured duration in PluginHealthExecutionResult.Duration (and CachedPluginHealthExecution.Duration), there is no architectural conflict with plugin supplied Data. A plugin may freely put its own diagnostic values (including its own latency estimate) into Data; that stays plugin data and is never reinterpreted as the host execution latency. No precedence rule is needed.

Health Result Caching

The host must cache completed PluginHealthExecutionResult values as CachedPluginHealthExecution for a configurable TTL. The cache must prevent every health request from executing the underlying plugin health check.

For example, with:

TTL = 30 seconds

multiple requests within the TTL should reuse the same completed health execution:

Request 1 -> Execute plugin -> Cache CachedPluginHealthExecution
Request 2 -> Cached result
Request 3 -> Cached result
Request 4 -> Cached result
...
TTL expires
Request N -> Execute plugin again

Cache Scope

Cached executions must be associated with the appropriate plugin and health check context. A result from one plugin must never be returned for another plugin.

If the host exposes multiple health check contexts or instances, cache keys must distinguish them appropriately.

Cache Expiration

When the TTL expires, the cached CachedPluginHealthExecution must no longer be considered valid. The next health check request must refresh the result by executing the plugin health check again.

Expired results must not be silently treated as permanently valid.

Configurable TTL

The cache TTL must be configurable by the host. The implementation must define clear default value if no explicit TTL is configured.

The default must prevent excessive repeated health check execution while avoiding excessively stale health information.

The exact default value may follow existing AuthKit host configuration conventions.

Disabled Caching

The host should support disabling health result caching where required.

A TTL of zero or an equivalent explicit configuration may be used to indicate that every request should execute fresh health check, provided this behavior is clearly defined by the host configuration.

The implementation must not accidentally interpret disabled cache as an infinite cache.

Concurrent Requests

The implementation should avoid unnecessary duplicate health checks when multiple requests arrive simultaneously after cache expiration.

For example:

Request A ─┐
Request B ─┼─ cache expired
Request C ─┘
      │
      ▼
One health check execution
      │
      ▼
Shared refreshed result

The host should prefer single flight or equivalent synchronization mechanism where practical.

However, synchronization must not introduce an unbounded lock or prevent unrelated plugins from executing their health checks concurrently.

If concurrent execution is intentionally allowed, this behavior must be explicitly documented and tested.

Cache and Cancellation

Cancellation of an individual health request must not corrupt the cached result.

If health check execution is cancelled before producing completed PluginHealthExecutionResult:

  • the incomplete operation must not be cached as successful result,
  • a fabricated Healthy result must not be inserted into the cache,
  • and the next eligible request must be able to perform fresh health check.

A previously valid cached CachedPluginHealthExecution may remain available according to the host's explicitly defined stale result policy, but cancellation must not transform an incomplete execution into a new valid cached result.

Failure Handling

If a plugin health check throws an exception, the host must handle the failure explicitly according to the existing health infrastructure.

The host must not silently convert arbitrary exceptions into:

Healthy

The resulting health state must remain distinguishable from successful health check.

If the host converts execution failures into Unhealthy, this behavior must be explicit and consistent.

Failed executions may be cached only if the host's health caching policy explicitly defines this behavior. (When cached, the entry is CachedPluginHealthExecution carrying the failed PluginHealthResult.)

Multiple Health Results

The host must preserve the multiple result semantics introduced in D3–D4.

If plugin returns:

Database       → Healthy
Cache          → Healthy
External API   → Degraded

the host must not collapse the results into an unrelated single boolean value.

Each PluginHealthResult must remain available inside PluginHealthExecutionResult.Results (and CachedPluginHealthExecution.Results) for the host aggregation layer.

Aggregation semantics belong to the host and must be handled explicitly.

Interaction With D5–D6 Metadata

The host must preserve, for every PluginHealthResult:

  • Status,
  • Reason,
  • Tags,
  • and Data

when building the execution wrapper and caching it.

Host generated latency lives in PluginHealthExecutionResult.Duration / CachedPluginHealthExecution.Duration - clearly separated from plugin defined Data. The host must not remove or silently rewrite plugin health tags or diagnostic information, and must not inject host latency into Data.

Thread Safety

The health result cache must be safe for concurrent access.

Multiple requests may access the health infrastructure concurrently.

The implementation must avoid:

  • race conditions,
  • corrupted cache entries,
  • cross plugin result contamination,
  • disposing scopes while they are still in use,
  • or returning partially constructed health results.

Backward Compatibility

The implementation must not require existing plugins to change solely to support host-side caching or scoped execution.

PluginHealthResult (D1–D4) keeps its shape: Status, Reason, Tags, Data. D7–D8 adds only host-side types (PluginHealthExecutionResult, CachedPluginHealthExecution); it does not modify the plugin contract.

Plugins that implement the structured health contract from D3–D4 must automatically benefit from host side execution infrastructure.

Existing plugins without custom structured health implementation must continue to use the compatibility behavior defined by the previous Section D tasks.

Existing plugin behavior outside health checks must remain unchanged.

DevTokens must continue to compile and load successfully.

Non Goals

This task does not include:

  • modifying PluginHealthResult (no Metadata or latency_ms field added to the plugin contract),
  • introducing new health status values,
  • redesigning IAuthKitPlugin,
  • defining new DI framework,
  • implementing custom caching framework,
  • introducing distributed health result caching,
  • changing plugin authentication behavior,
  • or redesigning the /health endpoint response contract beyond what is required to expose the new health information.

A simple in process host cache is sufficient unless existing AuthKit architecture requires otherwise.

Validation

Scoped Execution

  • Each uncached health check executes inside dedicated DI scope.
  • Scoped dependencies are resolved from the created scope.
  • The root provider is not used to resolve scoped health dependencies.
  • The scope is disposed after execution (success, failure, cancellation).
  • Async disposal is handled via CreateAsyncScope / await using where applicable.
  • No scoped dependency leaks between health check executions.

Latency

  • Host measures health check execution duration.
  • Measurement uses monotonic elapsed time mechanism.
  • Duration is exposed through PluginHealthExecutionResult.Duration (and CachedPluginHealthExecution.Duration), not through PluginHealthResult.Data.
  • Plugin code is not required to measure its own execution time.
  • Hostmeasured latency cannot be silently replaced by or merged into plugin Data.

Caching

  • Completed PluginHealthExecutionResult values are cached as CachedPluginHealthExecution after successful completion.
  • Cached results are returned within the configured TTL.
  • Expired results trigger a fresh health check.
  • Cache TTL is configurable.
  • A documented default TTL exists.
  • Cache can be explicitly disabled.
  • Cache keys distinguish independent plugins/health contexts.
  • Concurrent cache access is thread-safe.
  • Cache does not contain partially completed results.

Cancellation

  • Cancelled health checks are not cached as successful results.
  • Cancellation does not corrupt existing cache entries.
  • A subsequent request can execute a fresh check after a cancelled refresh.
  • Plugin cancellation behavior from D4 remains preserved.

Result Preservation

  • Multiple health results remain available (in Results).
  • Status is preserved.
  • Reason is preserved.
  • Tags are preserved.
  • Data is preserved.
  • Host-generated latency is present via Duration, separate from Data.

Failure Handling

  • Health check exceptions are handled explicitly.
  • Exceptions are never silently converted to Healthy.
  • Failure behavior is covered by tests.
  • Cache behavior after failed execution is explicitly tested.

Compatibility

  • Existing plugins continue to compile without modification.
  • DevTokens continues to compile and load successfully.
  • PluginContractValidator passes.
  • Existing non-health host behavior remains unchanged.

Build and Tests

  • dotnet build completes successfully with zero warnings.
  • Relevant unit tests pass.
  • Integration tests pass.
  • Scoped dependency tests pass.
  • Cache TTL tests pass.
  • Concurrent request tests pass.
  • Cancellation tests pass.
  • Latency measurement tests pass.

Acceptance Criteria

  • Every uncached plugin health check executes inside dedicated DI scope (CreateAsyncScope where async disposal is required).
  • Scoped dependencies are correctly resolved and disposed.
  • Health check execution latency is measured by the host.
  • Latency is exposed through PluginHealthExecutionResult.Duration / CachedPluginHealthExecution.Duration, never inside PluginHealthResult.Data.
  • Health executions are cached using a configurable TTL as CachedPluginHealthExecution.
  • Expired results trigger fresh health check.
  • Disabled caching is explicitly supported.
  • Concurrent requests cannot corrupt the cache or return results from another plugin.
  • Cancelled health checks do not create invalid cache entries.
  • Exceptions are handled explicitly and never silently reported as healthy.
  • Multiple health results and their metadata remain intact.
  • PluginHealthResult contract shape is unchanged.
  • Existing plugins remain compatible.
  • PluginContractValidator passes.
  • DevTokens continues to compile and load successfully.

Activity

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

Metadata

Metadata

Assignees

Labels

P0Contract foundationadditiveAdditive, non-breaking changearea/health-checksPlugin health check contract/infra (Section D)area/hostHost-side runtime (DI, OpenAPI, health exec)contractChanges the plugin contractsub-taskChild task of an epic

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions