Skip to content

Host-held adapter state, and fix pooled adapters sharing one configuration - #131

Merged
mmalkhatib merged 1 commit into
mainfrom
feat/adapter-state-and-pool-key
Sep 8, 2026
Merged

Host-held adapter state, and fix pooled adapters sharing one configuration#131
mmalkhatib merged 1 commit into
mainfrom
feat/adapter-state-and-pool-key

Conversation

@mmalkhatib

Copy link
Copy Markdown
Contributor

Two changes the resident runtime needs before Bitween's database adapters can exist. Both stand on their own.

1. Host-held adapter state

An adapter must not be the system of record for its own progress. The supervisor restarts it, the next instance may come up on a different node, and a pooled one is not the same process twice — so a polling receiver that keeps its cursor in a field replays from the beginning at the least convenient moment.

IAdapterContext gains two calls:

Task<string> GetStateAsync(string name, CancellationToken ct = default);
Task SetStateAsync(string name, string value, CancellationToken ct = default);  // null deletes

On the wire that is a new StateRequest (get / set / delete) answered by StateResult, correlated by frame id exactly as EventAck answers an Event. Host side it is IAdapterStateStore, the counterpart of IAdapterEventSink: where the sink is how an adapter hands work in, this is how it remembers where it got to. InMemoryAdapterStateStore is registered by default so samples and existing tests are untouched; a real deployment registers its own via the new AddResidentAdapters<TSink, TStateStore>().

Three deliberate properties, argued in design doc §14.9:

  • Keyed by instance, not adapter. Two instances of one adapter are two connections; one reading the other's cursor would skip rows that were never processed.
  • Not bounded by the in-flight window. That window stops an adapter flooding the host with events it must persist. A receiver saving its cursor after a batch would otherwise queue behind the very events whose progress it is recording.
  • Failures raised, not swallowed. A cursor that silently failed to save is a batch that will be replayed, and the adapter is the only thing positioned to stop rather than carry on.

This is the role Airbyte's state argument plays for its connectors.

2. A pool keyed by adapter id is a configuration leak

RentAsync keyed its pools on spec.AdapterId, and GetOrAdd captures the spec of whichever caller created the pool first — including its startup values, which is where the connection string and the credentials live. One adapter serving two data sources therefore handed the second one a process connected as the first, with no error anywhere.

Masked until now because bus providers run as exclusive instances keyed by data source and never go through the pool. Anything that rents — Bitween's Xchange pipeline does, through ResidentAdapterRuntime — was exposed, and a pooled database adapter would be exposed by design.

Fixed by keying on AdapterSpec.PoolKey when set, otherwise on the adapter id plus a hash of the startup values, so identical configuration shares warm processes and differing configuration cannot. Hashed rather than concatenated because the key reaches logs and diagnostics.

Compatibility

Additive. New proto fields take unused numbers, the default state store keeps existing hosts working with no code change, and no existing adapter or host behaviour changes. IAdapterContext gains members, so an out-of-tree implementation of that interface (test fakes, mostly) needs the two methods.

Tests

Three new tests in ResidentAdapterTests: state survives a restart of the adapter process, state is scoped to the instance, and pool keys separate specs that differ in configuration while staying free of credentials.

dotnet test — 82 passed, 1 failed: ResourceLimitTests.Lowering_the_soft_ceiling_takes_effect_without_a_restart. That one fails identically on a clean origin/main checkout on this machine, so it is pre-existing and not from this branch.

Docs: design doc §14.9 and §14.10, plus a line in docs/README.md.

🤖 Generated with Claude Code

…nfig

Two changes the resident runtime needs before a database adapter can exist,
both useful on their own.

Host-held state. An adapter must not be the system of record for its own
progress: the supervisor restarts it, the next instance may come up on another
node, and a pooled one is not the same process twice — so a polling receiver
that keeps its cursor in a field replays from the beginning at the least
convenient moment. IAdapterContext gains GetStateAsync / SetStateAsync,
answered over a new StateRequest / StateResult frame pair the same way
PublishAsync is answered by EventAck. Host side is IAdapterStateStore, the
counterpart of IAdapterEventSink; InMemoryAdapterStateStore keeps samples and
tests working untouched, and a real deployment registers its own through
AddResidentAdapters<TSink, TStateStore>(). State is keyed by instance, is not
bounded by the in-flight window, and raises rather than swallows failures —
see design doc 14.9 for why each of those matters.

Pool keying. RentAsync keyed its pools on spec.AdapterId, and GetOrAdd captures
the spec of whichever caller created the pool first, including its startup
values — where the connection string and credentials live. One adapter serving
two data sources therefore handed the second one a process connected as the
first, silently. Masked so far because bus providers run as exclusive instances
and never rent; anything going through ResidentAdapterRuntime was exposed.
Fixed by keying on AdapterSpec.PoolKey when set, otherwise on the adapter id
plus a hash of the startup values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: simplify9/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 85154c78-9b8f-4c5e-82c7-6970d790daa0

📥 Commits

Reviewing files that changed from the base of the PR and between cfc8da3 and d2e75ae.

📒 Files selected for processing (12)
  • SW.Serverless.Contract/Protos/adapter.proto
  • SW.Serverless.Samples.Ticker/Handler.cs
  • SW.Serverless.Sdk/Resident/IAdapterContext.cs
  • SW.Serverless.Sdk/Resident/ResidentRunner.cs
  • SW.Serverless.UnitTests/ResidentAdapterTests.cs
  • SW.Serverless/Extensions/IServiceCollectionExtensions.cs
  • SW.Serverless/Resident/AdapterSpec.cs
  • SW.Serverless/Resident/IAdapterStateStore.cs
  • SW.Serverless/Resident/ResidentAdapterHost.cs
  • SW.Serverless/Resident/ResidentAdapterInstance.cs
  • docs/README.md
  • docs/resident-adapters-design.md

📝 Summary

Summary

  • Adds host-managed adapter state through IAdapterContext.GetStateAsync and SetStateAsync.
  • Adds correlated StateRequest and StateResult protocol frames.
  • Adds pluggable IAdapterStateStore with an in-memory default and DI registration.
  • Scopes state by adapter instance and propagates storage failures.
  • Changes pooled adapter keys to use PoolKey or a hash of canonicalized startup values. This prevents configuration and credential mixing without exposing credentials in keys.
  • Updates the ticker sample and design documentation.

Risk: risk:medium

Security-sensitive areas

  • Pool isolation protects against cross-configuration and cross-credential process reuse.
  • State stores may contain cursors or other operational data. Hosts must select suitable durable storage and access controls.
  • Startup values are hashed for derived pool keys, but explicit PoolKey values remain caller-controlled.

Test coverage

  • Adds tests for state persistence across restart, instance isolation, and configuration-safe pool keys.
  • Reported result: 82 passing tests and one pre-existing failure in ResourceLimitTests.Lowering_the_soft_ceiling_takes_effect_without_a_restart.

Operational concerns

  • The default InMemoryAdapterStateStore does not preserve state across process restarts. Production hosts that require continuity must register a durable IAdapterStateStore.
  • Existing hosts must account for the updated ResidentAdapterHost dependency and protocol frames.
  • Rollback requires compatible handling of the additive protocol changes and consideration of persisted state data.

Walkthrough

The change adds host-held adapter state through correlated protocol frames, a pluggable state store, and new context APIs. It also isolates resident pools by explicit keys or startup-value hashes, with sample usage, tests, and documentation.

Changes

Adapter state and pool isolation

Layer / File(s) Summary
State protocol and context contract
SW.Serverless.Contract/Protos/adapter.proto, SW.Serverless.Sdk/Resident/IAdapterContext.cs
Adds StateRequest and StateResult protocol messages and exposes GetStateAsync and SetStateAsync.
State storage and runtime handling
SW.Serverless/Resident/IAdapterStateStore.cs, SW.Serverless/Extensions/IServiceCollectionExtensions.cs, SW.Serverless/Resident/ResidentAdapterHost.cs, SW.Serverless/Resident/ResidentAdapterInstance.cs, SW.Serverless.Sdk/Resident/ResidentRunner.cs
Adds keyed state storage, default and custom store registration, state request processing, correlated completions, cancellation, and storage error results.
Resident pool keying
SW.Serverless/Resident/AdapterSpec.cs, SW.Serverless/Resident/ResidentAdapterHost.cs
Adds explicit pool keys and derives hashed keys from canonicalized startup values when no explicit key is provided.
Usage, tests, and documentation
SW.Serverless.Samples.Ticker/Handler.cs, SW.Serverless.UnitTests/ResidentAdapterTests.cs, docs/README.md, docs/resident-adapters-design.md
Adds cursor commands, state persistence and isolation tests, pool-key tests, and documentation for state storage and pooling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Suggested labels: security, risk:high

Suggested reviewers: samerzughul


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmalkhatib
mmalkhatib merged commit c0efa5c into main Sep 8, 2026
4 of 5 checks passed
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.

2 participants