Skip to main content

AdapterPoolMemberService

Namespace: Meshmakers.Octo.Sdk.Common.Adapters

Runs this process as a member of an adapter pool (AB#4924): it opens the tenant-free management connection, registers on every connect, and heartbeats at the cadence the controller hands back.

public sealed class AdapterPoolMemberService : BackgroundService, IHostedService, IDisposable

Inheritance Object → BackgroundService → AdapterPoolMemberService
Implements IHostedService, IDisposable

Remarks:

This is the piece that turns AdapterPoolClient from a class into a running member. Increments 6 and 7 built the client, the hub, the scheduler and the work item, but nothing composed them into a process: AddAdapterPoolMember() registered the client and no hosted service ever started it, so a correctly configured member connected to nothing and was never leased.

🔴 Registration happens inside the connect callback, not once after StartAsync. The controller binds a member to its SignalR connection (AdapterPoolConnectionManager), so a reconnect leaves the old mapping behind. A member that registered only once would sit there connected and healthy-looking, and never receive another lease — the failure mode is silence, which is why this is a callback and not a line of startup code.

The heartbeat cadence is not a local option. The controller returns it at registration (PoolMemberRegistrationResultDto.HeartbeatIntervalSeconds), so the two sides cannot drift apart and an operator has exactly one place to change it. covers only the window before the first answer arrives, and the case where an older controller answers with nothing.

Member recovery (AB#4924 AP-I5), the pool member's counterpart of AB#5409/AB#5415. Registration — not the connection — is the signal, recorded in AdapterPoolClient.RegistrationState and read by AdapterPoolHubReadinessHealthCheck and AdapterPoolHubRecoveryService. This service adds the parts that only the member itself can do:

Re-register instead of heartbeating into nothing. A registration the controller refused (a controller pod that is shutting down, an enforcing tenant binding) or a heartbeat it rejected leaves the connection up and the member unleasable. The heartbeat tick then registers again rather than sending a heartbeat nobody records.

Never offer a busy process as free — and never lose its lease (AB#5826). A member that reconnects while its lease is still running registers through ResumePoolMemberAsync, naming the lease: the controller takes it over (after a controller restart it would otherwise not know it at all; after a dropped connection it holds the work back for a reconnect grace instead of re-queuing it), and the member's release then completes the execution. Only a controller that pre-dates the method (or refuses it) gets the old behaviour: the registration is deferred until the lease is released, because a plain registration would make it hand the busy member the next lease at once.

Report what could not be reported (AB#5826). A release that failed because the connection was down is kept and re-sent on the next connect, before the member registers again — so a controller that holds the work back for this member learns the outcome before it learns that the member is idle.

🔴 A draining member never registers again (AB#5864). It takes no further lease, and the controller only knows that for the connection the drain was reported on — a registration on a new connection starts as available, the next grant lands on a process that refuses it, and before AB#5864 that refusal failed the borrower's execution. The member exits instead (AdapterPoolMemberDrainExitService).

Flush the CK model cache of the recently leased tenants after a reconnect (AB#5415). CkModelChanged never reaches a member at all — it has no adapter-hub connection — so per-lease load/unload (the host's lease participants) is what keeps a member current between leases. What a reconnect can leave stale is the tenant of the lease running across it and, for a host without such a participant, the last tenant entered. Flushed before the registration, so no new lease can warm a cache that is about to be dropped.

Properties​

ExecuteTask​

public Task ExecuteTask { get; }

Property Value​

Task

Constructors​

AdapterPoolMemberService(AdapterPoolClient, IAdapterPoolHubClient, IOptions<AdapterPoolMemberOptions>, ILogger<AdapterPoolMemberService>, IAdapterService)​

Creates the pool-member service.

public AdapterPoolMemberService(AdapterPoolClient poolClient, IAdapterPoolHubClient hubClient, IOptions<AdapterPoolMemberOptions> options, ILogger<AdapterPoolMemberService> logger, IAdapterService adapterService)

Parameters​

poolClient AdapterPoolClient
The member's half of the lease protocol.

hubClient IAdapterPoolHubClient
The management connection.

options IOptions<AdapterPoolMemberOptions>
The pool this member belongs to.

logger ILogger<AdapterPoolMemberService>
Logger.

adapterService IAdapterService
The host's adapter service, whose IAdapterService.CkModelChangedAsync(String) drops a tenant's CK model cache. Optional: a host without one has no cache to flush.

Methods​

ExecuteAsync(CancellationToken)​

protected Task ExecuteAsync(CancellationToken stoppingToken)

Parameters​

stoppingToken CancellationToken

Returns​

Task

TickAsync()​

One heartbeat tick: a heartbeat while registered, a registration attempt while the connection is up but the controller holds no registration, nothing while the connection is down (the SignalR client owns the reconnect, and its connect callback registers). Internal so the decision can be tested without the timer.

internal Task TickAsync()

Returns​

Task

StopAsync(CancellationToken)​

public Task StopAsync(CancellationToken cancellationToken)

Parameters​

cancellationToken CancellationToken

Returns​

Task

OnConnectedAsync(Boolean)​

Runs on every connect and every reconnect. See the remark on the class for why registering once would be wrong. Internal for the tests.

internal Task OnConnectedAsync(bool reconnected)

Parameters​

reconnected Boolean

Returns​

Task