Zum Hauptinhalt springen

AdapterAccessTokenService

Namespace: Meshmakers.Octo.Sdk.Common.Adapters

Keeps the adapter's own service credential — the access token every /{tenantId}/adapterHub connection presents — current for the lifetime of the process (AB#5072).

public sealed class AdapterAccessTokenService : BackgroundService, IHostedService, IDisposable

Inheritance Object → BackgroundService → AdapterAccessTokenService
Implements IHostedService, IDisposable

Remarks:

The token is written into the process-wide that the adapter builders hand to AdapterHubClient. The SDK reads that object through HttpConnectionOptions.AccessTokenProvider on every connection attempt, so a token refreshed here is picked up by the next (re)connect without anything having to notify the client.

Why this service exists at all. Nothing filled that holder at startup. The only production writer was ServiceAccountTokenService.EnsureTokenAsync in octo-mesh-adapter, and every one of its callers sits on a pipeline execution path (DeployPipeline@1, AnthropicAiQuery@1, MeshContextCreatorService, PipelineIdentityResolver). So the holder was empty at connect time, the provider returned null, the connection went out with no Authorization header and the hub saw an anonymous caller that identified itself only through the unprotected adapter-rtId / adapter-ckTypeId headers. Worse than plainly anonymous: an adapter that had already run one of those pipelines did present a token on its next reconnect, so the fleet's state was not deterministic and the adapter-hub gate's (AB#5063) LogOnly inventory was not worth reading.

Why a refresh loop is needed even though the connection survives expiry. An established SignalR connection is authorized once, at connect time. The exposure is the reconnect — and an adapter reconnects routinely (controller rollout, node drain, network blip, the SDK's own retry loop, a wake from scale-to-zero). An adapter that acquired one token at startup would reconnect days later with a long-expired one and, under Enforce, be refused permanently.

Acquisition happens in AdapterAccessTokenService.StartAsync(CancellationToken), before the base class starts the loop. Hosted services are started sequentially, so registering this service before HostedAdapterExecutionService means the first hub connection already carries a token instead of racing the first acquisition.

Every failure is logged and swallowed. Refusing to start would turn a temporarily unreachable identity service into an adapter outage, and the connection itself is still valuable while the controller-side gate observes rather than enforces.

Fields​

RefreshSkew​

How long before its own expiry a token is replaced. Comfortably longer than a token request plus a reconnect, so a connection attempt never picks up a token that dies in flight.

public static TimeSpan RefreshSkew;

RetryInterval​

Floor for the sleep between refresh attempts. Also the cadence after a failed acquisition, so an adapter that came up before the identity service recovers on its own.

public static TimeSpan RetryInterval;

Properties​

ExecuteTask​

public Task ExecuteTask { get; }

Property Value​

Task

Constructors​

AdapterAccessTokenService(ILogger<AdapterAccessTokenService>, IOptions<AdapterOptions>, IServiceClientAccessToken, IAuthenticatorClient)​

Constructor.

public AdapterAccessTokenService(ILogger<AdapterAccessTokenService> logger, IOptions<AdapterOptions> options, IServiceClientAccessToken accessToken, IAuthenticatorClient authenticatorClient)

Parameters​

logger ILogger<AdapterAccessTokenService>
Logger.

options IOptions<AdapterOptions>
The adapter options carrying the client-credentials configuration.

accessToken IServiceClientAccessToken
The process-wide access-token holder the SignalR client reads.

authenticatorClient IAuthenticatorClient
The SDK authenticator client running the grant.

Methods​

StartAsync(CancellationToken)​

public Task StartAsync(CancellationToken cancellationToken)

Parameters​

cancellationToken CancellationToken

Returns​

Task

ExecuteAsync(CancellationToken)​

protected Task ExecuteAsync(CancellationToken stoppingToken)

Parameters​

stoppingToken CancellationToken

Returns​

Task

EnsureTokenAsync()​

Acquires a token unless the current one is still comfortably valid. Returns whether a usable token is in place afterwards.

internal Task<bool> EnsureTokenAsync()

Returns​

Task<Boolean>