Design: Transactional Hot-Reload and Reliable Last-Known-Good Rollback
Status
Proposed design
How to Read the Source References
Links under Current code touchpoints identify existing source files whose current behavior is being described and which will likely need to be refactored during implementation. They are not example implementations of the proposed design.
The proposed classes, interfaces, algorithms, and code fragments in this document describe the new implementation. The linked source files show where the current behavior lives so the future implementation can be grounded in the existing architecture.
Problem Statement
The current hot-reload pipeline treats a configuration as last-known-good as soon as configuration validation succeeds, before all dependent runtime services have successfully refreshed.
The current sequence is approximately:
- Parse the new configuration.
- Replace the loader's active
RuntimeConfig.
- Validate the new configuration.
- Save it as
LastValidRuntimeConfig.
- Raise ordered hot-reload events.
- Each dependent service mutates its live state.
Current code touchpoints
This creates two problems:
- A dependent service can fail after the new configuration has already become the LKG.
- Services that refreshed before the failure are not restored to their previous state.
RestoreLkgConfig() restores only the configuration reference. It does not restore metadata providers, authorization maps, query engines, GraphQL schemas, or other cached state.
Example Failure
Assume configuration A is active and fully functional.
Configuration B:
- passes ordinary runtime configuration validation;
- changes one or more entities;
- causes a later runtime component to fail while rebuilding.
An MCP-specific example would be two entities that normalize to the same tool name, such as ReadRecords colliding with the built-in read_records tool.
The existing sequence can result in:
- active
RuntimeConfig: B;
LastValidRuntimeConfig: B;
- query managers: B;
- metadata providers: B;
- authorization state: B;
- MCP registry: A because its refresh failed;
- other components: potentially either A or B.
If the next edit is malformed, restoring the LKG restores B rather than the last completely operational configuration A.
This is a general hot-reload problem, not an MCP-specific problem.
Goals
- Treat a configuration as active only after every critical runtime component can apply it.
- Define LKG as the last configuration successfully applied to the entire runtime.
- Build replacement component state without mutating live state.
- Publish prepared state only after every participant has prepared successfully.
- Restore all already-committed components if an unexpected commit failure occurs.
- Keep the previous runtime completely usable when parsing, validation, or preparation fails.
- Serialize overlapping file-change notifications.
- Signal configuration change tokens only after a successful commit.
- Distinguish critical transactional work from noncritical post-commit notifications.
- Produce clear structured logs for each reload generation and failure stage.
Non-Goals
The initial implementation does not need to provide:
- dynamic ASP.NET endpoint mapping;
- hot-reload of endpoint paths;
- hot-enabling services that were not registered at startup;
- perfect generation isolation for requests already in flight;
- rollback of external mutations performed outside DAB;
- persistence of LKG state across process restarts.
Settings that require endpoint or middleware reconstruction must be rejected as restart-required changes during hot-reload.
Required Invariants
After a hot-reload attempt completes, exactly one of these must be true:
Successful reload
- active configuration is the candidate;
- LKG is the candidate;
- every critical component contains candidate state;
- change-token subscribers are notified;
- noncritical post-commit observers are invoked.
Failed reload
- active configuration remains the previous configuration;
- LKG remains the previous configuration;
- every critical component contains previous state;
- candidate resources are disposed;
- no configuration change token is signaled.
A configuration that merely passes schema validation must not become LKG.
Proposed Architecture
1. Separate Candidate Configuration From Active Configuration
Loading a file for hot-reload must produce a candidate without immediately replacing the active configuration.
Introduce an immutable value such as:
public sealed record RuntimeConfigCandidate(
long Generation,
RuntimeConfig Config,
string SourcePath,
DateTimeOffset DetectedAt);
Add a candidate-loading API to RuntimeConfigLoader, for example:
public bool TryLoadCandidate(
string path,
[NotNullWhen(true)] out RuntimeConfigCandidate? candidate,
bool isDevelopmentMode,
DeserializationVariableReplacementSettings replacementSettings);
Candidate loading may:
- read the file;
- deserialize it;
- replace environment and Key Vault references;
- preserve startup-bound values where required;
- perform parse-level validation.
Candidate loading must not:
- replace the active
RuntimeConfig;
- modify the LKG;
- signal a change token;
- invoke component refresh events.
Remove the hot-reload dependence on:
IsNewConfigDetected;
IsNewConfigValidated;
DoesConfigNeedValidation().
These flags exist because the current loader mutates active state before validation. They should no longer be necessary once candidates are isolated.
2. Make RuntimeConfigProvider the Active State Owner
RuntimeConfigProvider is documented as the owner of active runtime configuration, but the mutable state currently lives primarily in RuntimeConfigLoader.
Move active-state publication behind RuntimeConfigProvider.
Suggested state:
private RuntimeConfig _activeConfig;
private RuntimeConfig _lastAppliedConfig;
private long _activeGeneration;
Expose internal operations such as:
internal void CommitCandidate(RuntimeConfigCandidate candidate);
internal RuntimeConfigSnapshot GetActiveSnapshot();
GetConfig() and TryGetConfig() must return only committed state.
Use atomic reference publication, such as Volatile.Read() and Interlocked.Exchange(), where appropriate.
Rename LastValidRuntimeConfig to LastAppliedRuntimeConfig or equivalent. “Valid” is ambiguous because schema-valid does not mean successfully applied.
3. Introduce a Hot-Reload Coordinator
Add a singleton service in Core:
public sealed class RuntimeConfigHotReloadCoordinator
Responsibilities:
- Serialize reload attempts.
- Load or receive the latest candidate.
- Validate hot-reload compatibility.
- Validate the candidate configuration.
- Prepare every transactional participant.
- Commit every prepared change.
- Publish the candidate through
RuntimeConfigProvider.
- Update the LKG.
- Signal configuration change tokens.
- Run post-commit observers.
- Roll back committed participants on unexpected commit failure.
- Dispose candidate or retired state correctly.
Use a SemaphoreSlim or a single-reader Channel to prevent concurrent reload transactions.
The file-watcher callback should enqueue a reload request and return quickly. It should not perform database initialization or other long-running work directly on the file-watcher thread.
Multiple notifications for the same file save should be coalesced. If a new notification arrives during a reload, process the latest file contents once the current attempt finishes.
4. Add Transactional Participant Contracts
Replace critical string-based hot-reload events with explicit transactional participants.
Suggested contracts:
public interface IHotReloadParticipant
{
string Name { get; }
HotReloadStage Stage { get; }
ValueTask<IPreparedHotReloadChange> PrepareAsync(
HotReloadContext context,
CancellationToken cancellationToken);
}
public interface IPreparedHotReloadChange : IAsyncDisposable
{
void Commit();
void Rollback();
}
PrepareAsync() may perform:
- validation;
- database metadata discovery;
- construction of replacement dictionaries;
- construction of replacement clients or engines;
- schema generation;
- any operation that can reasonably fail.
PrepareAsync() must not mutate live service state.
Commit() must:
- perform only in-memory publication;
- preferably be an
Interlocked.Exchange() or equivalent reference swap;
- perform no file, database, or network I/O;
- be designed not to throw.
Rollback() must:
- restore the exact previous state captured before commit;
- perform only in-memory publication;
- be safe when called after a partial commit sequence.
Resource ownership rules:
- Before commit, the prepared change owns candidate resources.
- After commit, the live service owns candidate resources.
- Previous resources must remain available until the complete transaction succeeds.
- After successful completion, previous resources may be disposed.
- After failed preparation, candidate resources must be disposed.
- After rollback, candidate resources must be disposed and previous resources retained.
5. Define Explicit Reload Stages
Replace ordering hidden in event invocation order with a typed stage model.
Suggested initial stages:
public enum HotReloadStage
{
QueryManagers = 100,
MetadataProviders = 200,
QueryEngines = 300,
MutationEngines = 400,
Documentation = 500,
Authorization = 600,
McpRegistry = 700,
GraphQLSchema = 800,
AuthenticationOptions = 900,
Logging = 1000
}
The coordinator prepares participants in stage order.
Rollback occurs in reverse commit order.
Participants in the same stage should run sequentially initially. Parallel preparation can be considered later only when dependencies are explicitly understood.
6. Add a Transaction-Local Preparation Context
Some candidate components depend on other candidate components. For example, candidate metadata providers may require candidate query-manager state.
HotReloadContext should contain:
public sealed class HotReloadContext
{
public RuntimeConfigSnapshot Current { get; }
public RuntimeConfigCandidate Candidate { get; }
public HotReloadPreparedState PreparedState { get; }
}
HotReloadPreparedState may provide typed transaction-local state:
public void Add<T>(T state);
public T GetRequired<T>();
This is not application DI. It exists only for one reload transaction and allows later preparation stages to consume state prepared by earlier stages without publishing it globally.
Dependencies between stages must be documented and covered by tests.
Transaction Workflow
Phase 1: Detect and Load
- File watcher detects a change.
- Reload request is queued.
- Coordinator takes the reload serialization lock.
- Loader reads the newest file contents.
- Loader creates a
RuntimeConfigCandidate.
- Active configuration remains unchanged.
Phase 2: Compatibility Validation
Compare the current and candidate configurations.
Reject changes to settings that cannot safely reload, such as:
- endpoint paths;
- middleware shape;
- initially unregistered services;
- host mode;
- other startup-bound settings discovered during implementation.
Return a clear restart-required error rather than accepting a configuration that does not match the running endpoint table.
Create a dedicated component such as:
public sealed class HotReloadCompatibilityValidator
The validator should report every incompatible property in one result where practical.
Phase 3: Candidate Validation
Validate the candidate directly.
Add candidate-aware validation APIs so RuntimeConfigValidator does not need the candidate to become globally active before validating it.
Separate validation into:
- static configuration validation;
- runtime preparation validation.
Validation that requires constructing metadata providers or contacting a database belongs in participant preparation.
Do not call SetLkgConfig() during this phase.
Phase 4: Prepare
For each participant in stage order:
- Call
PrepareAsync().
- Record the returned prepared change.
- Make any required prepared state available to later stages.
If a participant fails:
- Stop preparation.
- Dispose all prepared changes in reverse order.
- Keep active configuration and live services unchanged.
- Log the participant and exception.
- Do not signal change tokens.
Phase 5: Commit
Once every participant has prepared successfully:
- Enter the short commit section.
- Call
Commit() for each prepared change in stage order.
- Publish the candidate through
RuntimeConfigProvider.
- Set the candidate as
LastAppliedRuntimeConfig.
- Increment the active generation.
- Exit the commit section.
No validation, database work, or schema generation should happen during commit.
Phase 6: Unexpected Commit Failure
Although commits must be designed not to throw, handle unexpected failures defensively.
If commit fails:
- Do not publish the candidate configuration.
- Call
Rollback() on already-committed changes in reverse order.
- Dispose candidate state.
- Keep the previous configuration and LKG.
- Log a critical transaction failure.
If rollback itself fails, the runtime state is indeterminate. In that case:
- report unhealthy status;
- log at critical severity;
- stop the application gracefully rather than continue with unknown mixed state.
Phase 7: Post-Commit
Only after the transaction succeeds:
- Signal
RuntimeConfigProvider change tokens.
- Refresh
IOptionsMonitor consumers.
- Send MCP list-change notifications.
- Emit informational logs and telemetry.
- Dispose retired previous-generation resources.
Post-commit observer failure must be logged but must not roll back an already committed runtime. Any observer required for correctness belongs in the transactional participant set instead.
Change-Token Semantics
RuntimeConfigProvider.GetChangeToken() currently signals before the ordered component refresh pipeline completes.
Change this behavior so that the token means:
A complete new runtime generation was successfully committed.
The token must not mean merely:
A new file was detected or parsed.
This ensures JWT options and other change-token consumers never observe a candidate that later fails to apply.
Components to Convert
Query Manager Factory
Refactor QueryManagerFactory so it can build replacement dictionaries without assigning them to live fields.
Prepared state should include:
- query builders;
- query executors;
- database exception parsers.
Commit should atomically swap one containing state object instead of assigning three fields independently.
Current code touchpoint
Metadata Provider Factory
Do not clear the live provider dictionary before replacement providers initialize.
Preparation should:
- Build a separate provider dictionary.
- Initialize all providers.
- Collect initialization errors.
- Fail without touching live providers if initialization fails.
Commit should swap the complete provider snapshot.
Current code touchpoint
Query and Mutation Engine Factories
Build complete replacement factory state during preparation.
Commit each complete state with one reference swap.
Current code touchpoints
Authorization Resolver
Build the candidate permission map and explicit-role set separately.
Publish both inside one immutable authorization snapshot.
Current code touchpoint
OpenAPI Documentor
Generate candidate documentation without replacing the currently served document.
Publish only during commit.
Current code touchpoint
GraphQL Schema
Combine eviction, creation, and refresh into one transactional participant.
Preparation should build and validate the replacement schema without evicting the live schema.
Commit should publish the prepared schema or executor.
Do not evict the active schema until the replacement is known to be valid.
Current code touchpoints
Logging
Parse and validate candidate log-level configuration during preparation.
Apply it after critical runtime state commits.
Preserve existing production behavior where only supported logging changes are honored.
Authentication Options
Build and validate candidate JWT/authentication options before commit.
Signal the options change token only after the runtime transaction succeeds.
Current code touchpoint
MCP Tool Registry
The MCP registry should become a participant once MCP registry hot-reload is implemented.
Preparation should build and validate a complete registry snapshot.
Commit should atomically swap the snapshot.
A failed registry preparation must leave the previous registry active and cause the complete candidate reload to be rejected.
Other Cached Services
Before implementation, inventory every type that:
- subscribes to
HotReloadEventHandler;
- subscribes to
RuntimeConfigLoadedHandlers;
- subscribes to the runtime change token;
- caches values from
RuntimeConfig;
- is constructed conditionally from startup configuration.
For each type, classify it as:
- transactional participant;
- post-commit observer;
- direct dynamic reader requiring no refresh;
- startup-bound setting requiring restart;
- unsupported hot-reload behavior that must be documented.
This inventory is an explicit deliverable of the implementation.
Dependency Injection Registration
A participant must use the same singleton instance as the live service.
Avoid registering separate instances for the service interface and participant interface.
Use a pattern equivalent to:
services.AddSingleton<QueryManagerFactory>();
services.AddSingleton<IAbstractQueryManagerFactory>(
provider => provider.GetRequiredService<QueryManagerFactory>());
services.AddSingleton<IHotReloadParticipant>(
provider => provider.GetRequiredService<QueryManagerFactory>());
Alternatively, register a lightweight participant adapter that targets the existing singleton.
Handling Requests During Commit
Preparation may take time, but it does not affect live state.
The commit section should be very short because it consists only of reference swaps.
Optionally add a HotReloadCommitBarrier:
- requests beginning during the commit window wait for commit or rollback to finish;
- requests already in flight may finish using state they already resolved;
- the barrier is released after either successful commit or completed rollback.
Perfect per-request generation isolation is not required for the initial implementation, but no partial state may remain after the transaction finishes.
File-Watcher and Reload Queue Behavior
Implement a hosted reload-processing service or equivalent single-reader queue.
Required behavior:
- file-watcher callbacks enqueue work only;
- duplicate notifications are coalesced;
- only one reload transaction runs at a time;
- if another change arrives while processing, rerun against the newest file;
- shutdown cancels queued work;
- reload exceptions never terminate the file-watcher callback thread;
- every attempt receives a generation or correlation ID.
Late Configuration
The configuration endpoint uses RuntimeConfigLoadedHandlers, which has similar partial-application behavior.
The first implementation may focus on file hot-reload, but the long-term design should route late configuration through the same coordinator.
The coordinator must support:
- no active configuration, for first-time late configuration;
- an existing active configuration, for replacement;
- the same prepare/commit guarantees in both cases.
Do not maintain two independent application pipelines indefinitely.
Existing Event System
HotReloadEventHandler may remain temporarily for compatibility, but critical state mutation must move to transactional participants.
During migration:
- critical events must not run before commit;
- noncritical events may be invoked as post-commit observers;
- event exceptions must be isolated and logged;
- string event names should eventually be removed for transactional components.
Do not mix old mutating event handlers with new participants for the same component.
Logging and Telemetry
Log one structured record for each stage:
- candidate detected;
- candidate parsed;
- compatibility validation passed or failed;
- configuration validation passed or failed;
- participant preparation started;
- participant preparation completed;
- participant preparation failed;
- transaction commit started;
- transaction committed;
- rollback started;
- rollback completed;
- post-commit observer failed.
Include:
- reload generation;
- configuration path;
- participant name;
- stage;
- elapsed time;
- previous active generation;
- candidate generation;
- failure exception;
- final transaction result.
Expose the latest reload status to health diagnostics where practical.
Failure Semantics
| Failure stage |
Active config |
LKG |
Component state |
Change token |
| File read or parse |
Previous |
Previous |
Previous |
Not signaled |
| Compatibility validation |
Previous |
Previous |
Previous |
Not signaled |
| Config validation |
Previous |
Previous |
Previous |
Not signaled |
| Participant preparation |
Previous |
Previous |
Previous |
Not signaled |
| Commit with successful rollback |
Previous |
Previous |
Previous |
Not signaled |
| Commit with failed rollback |
Indeterminate; stop app |
Previous |
Indeterminate |
Not signaled |
| Post-commit observer |
Candidate |
Candidate |
Candidate |
Already signaled |
Testing Plan
Candidate Loading Tests
- Parsing a candidate does not replace active configuration.
- Invalid JSON leaves active configuration and LKG unchanged.
- Environment-variable replacement occurs in the candidate only.
- Production-mode filtering creates an effective candidate without mutating active state.
Coordinator Tests
- Participants prepare in stage order.
- Participants commit in stage order.
- Preparation failure prevents every commit.
- Prepared changes are disposed in reverse order after failure.
- Commit failure rolls back committed participants in reverse order.
- Active config is published only after all participant commits.
- LKG updates only after complete success.
- Change token fires exactly once after success.
- Change token does not fire after any failure.
- Post-commit failure does not roll back committed state.
- Concurrent reload requests are serialized.
- Duplicate file events are coalesced.
Participant Tests
For each participant:
- preparation does not mutate live state;
- successful commit publishes all related fields atomically;
- rollback restores the exact prior snapshot;
- failed preparation disposes candidate resources;
- previous resources are retained until transaction completion.
Integration Tests
- Start with configuration A.
- Hot-reload valid configuration B.
- Verify every runtime component reflects B.
- Hot-reload malformed configuration C.
- Verify every runtime component still reflects B.
- Hot-reload schema-valid configuration D that causes a participant preparation failure.
- Verify every runtime component and LKG still reflect B.
- Correct D and verify the next reload succeeds.
- Trigger rapid consecutive file writes and verify the final file wins.
- Verify restart-required property changes are rejected without altering active state.
Fault-Injection Tests
Add test participants that fail during:
- preparation;
- first commit;
- middle commit;
- rollback;
- disposal;
- post-commit notification.
Use these tests to prove coordinator state-machine behavior independently of databases.
Implementation Order
- Add characterization tests demonstrating the existing LKG/partial-state problem.
- Add candidate-loading APIs that do not mutate active state.
- Move active configuration ownership into
RuntimeConfigProvider.
- Add
HotReloadCompatibilityValidator.
- Add participant and prepared-change contracts.
- Add the serialized coordinator and reload queue.
- Convert query-manager state.
- Convert metadata-provider state.
- Convert query and mutation engines.
- Convert authorization state.
- Convert OpenAPI and GraphQL state.
- Convert authentication and logging behavior.
- Move change-token signaling to post-commit.
- Route file-watcher reloads through the coordinator.
- Convert or isolate remaining event subscribers.
- Add MCP registry participation when its hot-reload implementation is available.
- Route late configuration through the same coordinator.
- Remove obsolete validation flags and mutating hot-reload event paths.
- Update the hot-reload design documentation.
Do not switch production hot-reload orchestration to the coordinator until every currently critical event subscriber has either become a participant or been explicitly classified as post-commit/restart-bound.
Acceptance Criteria
The work is complete when:
- A candidate cannot become active or LKG before all critical participants prepare successfully.
- A participant preparation failure leaves the complete previous runtime operational.
- An unexpected commit failure restores every already-committed participant.
RestoreLkgConfig() is no longer the sole rollback mechanism.
- The LKG always represents the last fully applied runtime generation.
- Critical components no longer clear or mutate live state during preparation.
- Change tokens are emitted only after successful transaction commit.
- Unsupported startup-bound changes are rejected with a restart-required diagnostic.
- Overlapping file notifications cannot run overlapping transactions.
- Integration and fault-injection tests cover parse, validation, preparation, commit, rollback, and post-commit failures.
Key Design Principle
Hot-reload must be treated as a state transition:
Current Generation -> Prepared Candidate -> Committed Generation
It must not be treated as a sequence of independent mutation callbacks.
Validation determines whether a candidate is structurally acceptable.
Preparation determines whether the complete runtime can support it.
Commit makes it active.
Only a successfully committed generation is last-known-good.
Design: Transactional Hot-Reload and Reliable Last-Known-Good Rollback
Status
Proposed design
How to Read the Source References
Links under Current code touchpoints identify existing source files whose current behavior is being described and which will likely need to be refactored during implementation. They are not example implementations of the proposed design.
The proposed classes, interfaces, algorithms, and code fragments in this document describe the new implementation. The linked source files show where the current behavior lives so the future implementation can be grounded in the existing architecture.
Problem Statement
The current hot-reload pipeline treats a configuration as last-known-good as soon as configuration validation succeeds, before all dependent runtime services have successfully refreshed.
The current sequence is approximately:
RuntimeConfig.LastValidRuntimeConfig.Current code touchpoints
This creates two problems:
RestoreLkgConfig()restores only the configuration reference. It does not restore metadata providers, authorization maps, query engines, GraphQL schemas, or other cached state.Example Failure
Assume configuration A is active and fully functional.
Configuration B:
An MCP-specific example would be two entities that normalize to the same tool name, such as
ReadRecordscolliding with the built-inread_recordstool.The existing sequence can result in:
RuntimeConfig: B;LastValidRuntimeConfig: B;If the next edit is malformed, restoring the LKG restores B rather than the last completely operational configuration A.
This is a general hot-reload problem, not an MCP-specific problem.
Goals
Non-Goals
The initial implementation does not need to provide:
Settings that require endpoint or middleware reconstruction must be rejected as restart-required changes during hot-reload.
Required Invariants
After a hot-reload attempt completes, exactly one of these must be true:
Successful reload
Failed reload
A configuration that merely passes schema validation must not become LKG.
Proposed Architecture
1. Separate Candidate Configuration From Active Configuration
Loading a file for hot-reload must produce a candidate without immediately replacing the active configuration.
Introduce an immutable value such as:
Add a candidate-loading API to
RuntimeConfigLoader, for example:Candidate loading may:
Candidate loading must not:
RuntimeConfig;Remove the hot-reload dependence on:
IsNewConfigDetected;IsNewConfigValidated;DoesConfigNeedValidation().These flags exist because the current loader mutates active state before validation. They should no longer be necessary once candidates are isolated.
2. Make
RuntimeConfigProviderthe Active State OwnerRuntimeConfigProvideris documented as the owner of active runtime configuration, but the mutable state currently lives primarily inRuntimeConfigLoader.Move active-state publication behind
RuntimeConfigProvider.Suggested state:
Expose internal operations such as:
GetConfig()andTryGetConfig()must return only committed state.Use atomic reference publication, such as
Volatile.Read()andInterlocked.Exchange(), where appropriate.Rename
LastValidRuntimeConfigtoLastAppliedRuntimeConfigor equivalent. “Valid” is ambiguous because schema-valid does not mean successfully applied.3. Introduce a Hot-Reload Coordinator
Add a singleton service in Core:
Responsibilities:
RuntimeConfigProvider.Use a
SemaphoreSlimor a single-readerChannelto prevent concurrent reload transactions.The file-watcher callback should enqueue a reload request and return quickly. It should not perform database initialization or other long-running work directly on the file-watcher thread.
Multiple notifications for the same file save should be coalesced. If a new notification arrives during a reload, process the latest file contents once the current attempt finishes.
4. Add Transactional Participant Contracts
Replace critical string-based hot-reload events with explicit transactional participants.
Suggested contracts:
PrepareAsync()may perform:PrepareAsync()must not mutate live service state.Commit()must:Interlocked.Exchange()or equivalent reference swap;Rollback()must:Resource ownership rules:
5. Define Explicit Reload Stages
Replace ordering hidden in event invocation order with a typed stage model.
Suggested initial stages:
The coordinator prepares participants in stage order.
Rollback occurs in reverse commit order.
Participants in the same stage should run sequentially initially. Parallel preparation can be considered later only when dependencies are explicitly understood.
6. Add a Transaction-Local Preparation Context
Some candidate components depend on other candidate components. For example, candidate metadata providers may require candidate query-manager state.
HotReloadContextshould contain:HotReloadPreparedStatemay provide typed transaction-local state:This is not application DI. It exists only for one reload transaction and allows later preparation stages to consume state prepared by earlier stages without publishing it globally.
Dependencies between stages must be documented and covered by tests.
Transaction Workflow
Phase 1: Detect and Load
RuntimeConfigCandidate.Phase 2: Compatibility Validation
Compare the current and candidate configurations.
Reject changes to settings that cannot safely reload, such as:
Return a clear restart-required error rather than accepting a configuration that does not match the running endpoint table.
Create a dedicated component such as:
The validator should report every incompatible property in one result where practical.
Phase 3: Candidate Validation
Validate the candidate directly.
Add candidate-aware validation APIs so
RuntimeConfigValidatordoes not need the candidate to become globally active before validating it.Separate validation into:
Validation that requires constructing metadata providers or contacting a database belongs in participant preparation.
Do not call
SetLkgConfig()during this phase.Phase 4: Prepare
For each participant in stage order:
PrepareAsync().If a participant fails:
Phase 5: Commit
Once every participant has prepared successfully:
Commit()for each prepared change in stage order.RuntimeConfigProvider.LastAppliedRuntimeConfig.No validation, database work, or schema generation should happen during commit.
Phase 6: Unexpected Commit Failure
Although commits must be designed not to throw, handle unexpected failures defensively.
If commit fails:
Rollback()on already-committed changes in reverse order.If rollback itself fails, the runtime state is indeterminate. In that case:
Phase 7: Post-Commit
Only after the transaction succeeds:
RuntimeConfigProviderchange tokens.IOptionsMonitorconsumers.Post-commit observer failure must be logged but must not roll back an already committed runtime. Any observer required for correctness belongs in the transactional participant set instead.
Change-Token Semantics
RuntimeConfigProvider.GetChangeToken()currently signals before the ordered component refresh pipeline completes.Change this behavior so that the token means:
The token must not mean merely:
This ensures JWT options and other change-token consumers never observe a candidate that later fails to apply.
Components to Convert
Query Manager Factory
Refactor
QueryManagerFactoryso it can build replacement dictionaries without assigning them to live fields.Prepared state should include:
Commit should atomically swap one containing state object instead of assigning three fields independently.
Current code touchpoint
Metadata Provider Factory
Do not clear the live provider dictionary before replacement providers initialize.
Preparation should:
Commit should swap the complete provider snapshot.
Current code touchpoint
Query and Mutation Engine Factories
Build complete replacement factory state during preparation.
Commit each complete state with one reference swap.
Current code touchpoints
Authorization Resolver
Build the candidate permission map and explicit-role set separately.
Publish both inside one immutable authorization snapshot.
Current code touchpoint
OpenAPI Documentor
Generate candidate documentation without replacing the currently served document.
Publish only during commit.
Current code touchpoint
GraphQL Schema
Combine eviction, creation, and refresh into one transactional participant.
Preparation should build and validate the replacement schema without evicting the live schema.
Commit should publish the prepared schema or executor.
Do not evict the active schema until the replacement is known to be valid.
Current code touchpoints
Logging
Parse and validate candidate log-level configuration during preparation.
Apply it after critical runtime state commits.
Preserve existing production behavior where only supported logging changes are honored.
Authentication Options
Build and validate candidate JWT/authentication options before commit.
Signal the options change token only after the runtime transaction succeeds.
Current code touchpoint
MCP Tool Registry
The MCP registry should become a participant once MCP registry hot-reload is implemented.
Preparation should build and validate a complete registry snapshot.
Commit should atomically swap the snapshot.
A failed registry preparation must leave the previous registry active and cause the complete candidate reload to be rejected.
Other Cached Services
Before implementation, inventory every type that:
HotReloadEventHandler;RuntimeConfigLoadedHandlers;RuntimeConfig;For each type, classify it as:
This inventory is an explicit deliverable of the implementation.
Dependency Injection Registration
A participant must use the same singleton instance as the live service.
Avoid registering separate instances for the service interface and participant interface.
Use a pattern equivalent to:
Alternatively, register a lightweight participant adapter that targets the existing singleton.
Handling Requests During Commit
Preparation may take time, but it does not affect live state.
The commit section should be very short because it consists only of reference swaps.
Optionally add a
HotReloadCommitBarrier:Perfect per-request generation isolation is not required for the initial implementation, but no partial state may remain after the transaction finishes.
File-Watcher and Reload Queue Behavior
Implement a hosted reload-processing service or equivalent single-reader queue.
Required behavior:
Late Configuration
The configuration endpoint uses
RuntimeConfigLoadedHandlers, which has similar partial-application behavior.The first implementation may focus on file hot-reload, but the long-term design should route late configuration through the same coordinator.
The coordinator must support:
Do not maintain two independent application pipelines indefinitely.
Existing Event System
HotReloadEventHandlermay remain temporarily for compatibility, but critical state mutation must move to transactional participants.During migration:
Do not mix old mutating event handlers with new participants for the same component.
Logging and Telemetry
Log one structured record for each stage:
Include:
Expose the latest reload status to health diagnostics where practical.
Failure Semantics
Testing Plan
Candidate Loading Tests
Coordinator Tests
Participant Tests
For each participant:
Integration Tests
Fault-Injection Tests
Add test participants that fail during:
Use these tests to prove coordinator state-machine behavior independently of databases.
Implementation Order
RuntimeConfigProvider.HotReloadCompatibilityValidator.Do not switch production hot-reload orchestration to the coordinator until every currently critical event subscriber has either become a participant or been explicitly classified as post-commit/restart-bound.
Acceptance Criteria
The work is complete when:
RestoreLkgConfig()is no longer the sole rollback mechanism.Key Design Principle
Hot-reload must be treated as a state transition:
Current Generation -> Prepared Candidate -> Committed GenerationIt must not be treated as a sequence of independent mutation callbacks.
Validation determines whether a candidate is structurally acceptable.
Preparation determines whether the complete runtime can support it.
Commit makes it active.
Only a successfully committed generation is last-known-good.