Skip to content

feat(runtime): refresh type manifests on hot reload - #10947

Open
koenbeuk wants to merge 9 commits into
dotnet:mainfrom
koenbeuk:feature/hot-reload-manifest-refresh
Open

koenbeuk wants to merge 9 commits into
dotnet:mainfrom
koenbeuk:feature/hot-reload-manifest-refresh

Conversation

@koenbeuk

@koenbeuk koenbeuk commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Everything derived from the generated type manifests is captured once at startup: codec maps, type-name allow lists, the grain class map, RPC proxy mappings, and the silo manifest. A Hot Reload edit regenerates Metadata_{Assembly} in place, but nothing re-reads it — so a type or grain added while the process runs fails with CodecNotFoundException or an unresolvable grain interface until restart, even though its generated codec and proxy already exist.

Solution

  • Register a MetadataUpdateHandler that runs after an update batch is applied, filters to assemblies bearing TypeManifestProviderAttribute, and fans out to per-container refresh participants in two phases; the handler never throws.
  • Re-run the regenerated manifest providers against a scratch TypeManifestOptions and merge additively into the live options, keeping the refresh idempotent.
  • Refresh serialization state: rebuild CodecProvider metadata maps and clear its caches (including cached AbstractTypeSerializer fallbacks), rebuild TypeConverter alias maps and purge negative type-name verdicts, rebuild WellKnownTypeCollection, and clear TypeCodec and generated accessor caches.
  • Refresh the silo: rebuild the silo manifest and grain class map, republish the local cluster manifest with a minor version bump, rebuild the RPC proxy mapping, and purge version- and interface-resolution caches. ClusterManifestSystemTarget now serves the live local manifest.
  • Refresh hot-reloaded client processes through an equivalent client-side participant.
  • Register everything only when MetadataUpdater.IsSupported, keeping production startup and steady state unchanged.
  • Cover the refresh with serializer-level unit tests and an end-to-end cluster test in which a grain withheld at startup becomes callable after a refresh.

With #10932's generation shape (<OrleansHotReload>true</OrleansHotReload>), adding new [GenerateSerializer] types, grain interfaces, and grain classes now works without a restart in a running silo. Grain interface changes remain rude edits by EnC rules, direct wire serialization of a hot-reload-added type is blocked by a .NET runtime constraint-verification limitation (grain calls are unaffected), and peer silos still learn of manifest changes only through their existing refresh channels — a follow-up will address peer re-fetch.

Microsoft Reviewers: Open in CodeFlow

Copilot AI lite review requested due to automatic review settings September 2, 2026 01:16
@koenbeuk
koenbeuk requested a review from ReubenBond as a code owner September 2, 2026 01:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several refreshed/swap-updated maps/manifests are published with Volatile.Write (or plain assignment) but read without volatile semantics, risking stale reads across threads after hot reload.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds runtime support for refreshing Orleans’ generated type/grain manifests after a .NET Hot Reload metadata update, so newly added [GenerateSerializer] types, grain interfaces, and grain classes become usable without restarting silos/clients.

Changes:

  • Introduces a MetadataUpdateHandler-driven refresh pipeline with per-container participants (serialization first, then grain metadata) and opt-in registration when MetadataUpdater.IsSupported.
  • Rebuilds/merges manifest-derived state (codecs, type-name allow/alias maps, well-known types, grain class maps, RPC proxy mappings, and local cluster/client manifests).
  • Adds unit + end-to-end cluster tests covering refresh behavior and idempotency.
File summaries
File Description
test/Orleans.Serialization.UnitTests/HotReloadScenarioTypes.cs Adds serializer scenario types used by refresh tests.
test/Orleans.Serialization.UnitTests/HotReloadRefreshTests.cs Adds unit tests simulating refresh restoring serializer/type-name behavior.
test/Orleans.DefaultCluster.Tests/HotReloadGrainTests.cs Adds an end-to-end test validating newly added grains become callable after refresh.
src/Orleans.Serialization/TypeSystem/TypeConverter.cs Adds manifest re-application + negative-cache purge on refresh.
src/Orleans.Serialization/TypeSystem/TypeCodec.cs Adds cache clearing for formatted type-name/type-key caches after refresh.
src/Orleans.Serialization/TypeSystem/CompoundTypeAliasTree.cs Adds copy-on-write merge support for compound type aliases.
src/Orleans.Serialization/Session/WellKnownTypeCollection.cs Rebuilds well-known type id maps on manifest refresh.
src/Orleans.Serialization/Serializers/CodecProvider.cs Rebuilds codec metadata maps and clears caches after refresh; wires refresher construction for hot reload.
src/Orleans.Serialization/Orleans.Serialization.csproj Expands InternalsVisibleTo to enable runtime/client refreshers to call internal refresh hooks.
src/Orleans.Serialization/Hosting/ServiceCollectionExtensions.cs Registers the serialization hot reload refresher when supported.
src/Orleans.Serialization/Hosting/SerializationHotReloadRefresher.cs Implements serialization-layer manifest re-run/merge and derived-cache refresh.
src/Orleans.Serialization/Hosting/HotReloadMetadataUpdateHandler.cs Adds the metadata update handler + participant fan-out infrastructure.
src/Orleans.Runtime/Orleans.Runtime.csproj Adds InternalsVisibleTo for DefaultCluster.Tests.
src/Orleans.Runtime/Manifest/SiloManifestProvider.cs Allows rebuilding silo manifest/grain map after refresh.
src/Orleans.Runtime/Manifest/SiloHotReloadRefresher.cs Adds silo-side refresh participant to rebuild/publish updated grain metadata and clear resolver caches.
src/Orleans.Runtime/Manifest/GrainClassMap.cs Enables in-place grain class map updates after refresh.
src/Orleans.Runtime/Manifest/ClusterManifestProvider.cs Publishes updated local manifest with minor version bump after refresh.
src/Orleans.Runtime/Hosting/DefaultSiloServices.cs Registers silo hot reload refresher when supported.
src/Orleans.Runtime/GrainTypeManager/ClusterManifestSystemTarget.cs Serves the live local manifest (so peers/clients can observe refresh).
src/Orleans.Core/Manifest/GrainVersionManifest.cs Clears version/generic caches on local manifest updates.
src/Orleans.Core/Manifest/ClientManifestProvider.cs Allows client manifest to be rebuilt after refresh.
src/Orleans.Core/Manifest/ClientHotReloadRefresher.cs Adds client-side refresh participant to rebuild/publish local client manifest and clear caches.
src/Orleans.Core/Manifest/ClientClusterManifestProvider.cs Allows replacing the local client manifest after refresh.
src/Orleans.Core/GrainReferences/GrainReferenceActivator.cs Rebuilds the RPC proxy map after refresh.
src/Orleans.Core/Core/GrainInterfaceTypeToGrainTypeResolver.cs Clears generic mapping cache after refresh.
src/Orleans.Core/Core/DefaultClientServices.cs Registers client hot reload refresher when supported.
Orleans.slnx Adds the HotReloadSpike playground project to the solution.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 9
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Orleans.Core/GrainReferences/GrainReferenceActivator.cs
Comment thread src/Orleans.Core/Manifest/ClientClusterManifestProvider.cs
Comment thread src/Orleans.Core/Manifest/GrainVersionManifest.cs
Comment thread src/Orleans.Runtime/Manifest/ClusterManifestProvider.cs Outdated
Comment thread src/Orleans.Runtime/Manifest/GrainClassMap.cs
Comment thread src/Orleans.Serialization/Serializers/CodecProvider.cs Outdated
Comment thread src/Orleans.Serialization/Session/WellKnownTypeCollection.cs Outdated
Comment thread src/Orleans.Serialization/TypeSystem/TypeConverter.cs Outdated
Comment thread test/Orleans.Serialization.UnitTests/HotReloadScenarioTypes.cs
Copilot AI review requested due to automatic review settings September 2, 2026 09:47
@ReubenBond

Copy link
Copy Markdown
Member

CI was failing across the matrix because this branch added \playground/HotReloadSpike/HotReloadSpike.csproj\ to \Orleans.slnx\ without including that project, producing MSB3202 before builds/tests could run. Commit \3bdfa41997\ removes the stale solution entry and also addresses all hot-reload publication review feedback. The full solution builds and the focused serialization and grain hot-reload suites pass locally; fresh CI is now running.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

There are a couple of correctness/build-blocking issues (unnecessary using likely triggering IDE0005, and missing volatile publication semantics for compound alias tree readers) which should be addressed before merging.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/Orleans.Serialization/TypeSystem/CompoundTypeAliasTree.cs:5

  • MergeFrom publishes _children using Volatile.Write, but _children itself is not declared volatile, so reads in TryGetChild are not guaranteed to have acquire semantics. Marking _children as volatile ensures safe publication of the copy-on-write dictionary on weak memory architectures.
    src/Orleans.Serialization/Hosting/HotReloadMetadataUpdateHandler.cs:8
  • using Orleans.Serialization.Hosting; is unnecessary here (the file is already in namespace Orleans.Serialization.Hosting;) and can trigger IDE0005 (unnecessary using) when code style is enforced as errors.
  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from 3bdfa41 to a334e3c Compare September 4, 2026 11:03
Copilot AI review requested due to automatic review settings September 4, 2026 11:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

At least one new unit test as written cannot pass because its type-name filter permanently denies the target type even after refresh.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

test/Orleans.Serialization.UnitTests/HotReloadRefreshTests.cs:92

  • ScenarioTypeNameFilter currently denies all matching type names permanently, which prevents this test from ever recovering after refresh. Make the filter behavior switchable so the test can simulate a filter policy change (or external allow-list update) across a hot reload batch.
    private sealed class ScenarioTypeNameFilter : Orleans.Serialization.ITypeNameFilter
    {
        public bool? IsTypeNameAllowed(string typeName, string assemblyName)
            => typeName is not null && typeName.Contains(ScenarioNamespaceFragment, StringComparison.Ordinal) ? false : null;
    }
  • Files reviewed: 26/26 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread test/Orleans.Serialization.UnitTests/HotReloadRefreshTests.cs
Comment thread src/Orleans.Serialization/Hosting/HotReloadMetadataUpdateHandler.cs Outdated
@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Code coverage

Metric Pull request Current main Variance
Lines 81.79% (111,994 / 136,926) 82.01% (111,893 / 136,444) -0.2149 pp
Branches 71.06% (32,179 / 45,284) 71.17% (32,155 / 45,178) -0.1136 pp

Report-only conclusion: regressed.

The current-main baseline is commit 9222be9a19 and uses the same reviewed coverage matrix.

Coverage combines every CI test matrix job, including providers, CodeGen, .NET 8/10, Linux, Windows, and macOS, using canonical physical source and branch identities.

The comparison remains report-only while normal line and branch variance is calibrated.

Coverage details

Copilot AI review requested due to automatic review settings September 4, 2026 12:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A confirmed thread-visibility issue in ClusterManifestProvider.OnLocalManifestUpdated can prevent publishing the refreshed manifest under concurrent updates unless _current is read with volatile semantics.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Orleans.Runtime/Manifest/ClusterManifestProvider.cs:83

  • OnLocalManifestUpdated reads the evolving _current manifest using a plain read. Since _current is concurrently updated via Interlocked.Exchange in the AsyncEnumerable onPublished callback, using Volatile.Read here avoids stale reads that can cause TryPublishManifest to repeatedly propose a version which is not actually > the latest published manifest (and therefore fail the updateValidator), potentially preventing the local manifest update from being published.
  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 4, 2026 17:16
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from 87a82f0 to a2b503a Compare September 4, 2026 17:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There is at least one confirmed compile-time issue in the new cluster test and a confirmed concurrency hazard from mutating shared TypeManifestOptions collections in place during refresh.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 26/26 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/Orleans.Serialization/Hosting/SerializationHotReloadRefresher.cs Outdated
Comment thread test/Orleans.DefaultCluster.Tests/HotReloadGrainTests.cs
Copilot AI review requested due to automatic review settings September 4, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The refreshed manifest merge and TypeConverter update path have a correctness gap (compound type alias updates not being observed) plus a merge behavior which can silently overwrite well-known mappings during refresh.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/Orleans.Serialization/Configuration/TypeManifestOptions.cs
Comment thread src/Orleans.Serialization/TypeSystem/TypeConverter.cs
Copilot AI review requested due to automatic review settings September 4, 2026 19:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces broad runtime/serialization manifest refresh behavior with concurrency-sensitive cache publication and cluster/client manifest versioning changes that warrant careful human validation beyond automated review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/Orleans.Serialization/Hosting/HotReloadMetadataUpdateHandler.cs:8

  • using Orleans.Serialization.Hosting; is redundant here since the file is already in the Orleans.Serialization.Hosting namespace, and the directive is unused. Removing it avoids unnecessary imports and potential IDE0005 diagnostics during build.
    test/Orleans.DefaultCluster.Tests/HotReloadGrainTests.cs:20
  • Several using directives at the top of the file appear to be unused (e.g., Microsoft.Extensions.Options, Orleans.GrainReferences, Orleans.Metadata, Orleans.Runtime.Versions, and the extra Orleans.Serialization namespaces). Removing them helps keep the test clean and avoids potential build-time style diagnostics.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 5, 2026 09:37
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from 8229cd2 to f5aacf1 Compare September 5, 2026 09:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are warnings-as-errors build risks from unused using directives in newly added/modified files which should be cleaned up before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/Orleans.Serialization/Hosting/HotReloadMetadataUpdateHandler.cs
Comment thread test/Orleans.DefaultCluster.Tests/HotReloadGrainTests.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The change spans Orleans serialization + runtime manifest publication with concurrency-sensitive copy-on-write behavior, so it warrants final human review despite solid test coverage.

Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 7, 2026 09:53
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from 1157811 to 5bac50c Compare September 7, 2026 09:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes core runtime/serialization metadata derivation and cross-thread cache publication behavior, which warrants final human review despite strong test coverage.

Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 10, 2026 15:38
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from 5bac50c to c95a0f3 Compare September 10, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The metadata update handler currently caches “no manifest provider attribute” results in a way which can become stale under hot reload, preventing refresh when an assembly gains the manifest provider attribute.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +72 to +76
var assembly = type.Assembly;
if (ManifestAssemblyCache.GetOrAdd(assembly, static a => a.IsDefined(typeof(TypeManifestProviderAttribute))))
{
updatedAssemblies.Add(assembly);
}
Copilot AI review requested due to automatic review settings September 10, 2026 19:44
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from c95a0f3 to 32c54de Compare September 10, 2026 19:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes core runtime/serialization metadata flows and concurrency-visible caches, so it needs final human validation despite only minor review comments.

Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +14 to +16
namespace DefaultCluster.Tests
{
/// <summary>
Comment on lines +39 to +43
Assert.ThrowsAny<Exception>(() =>
{
var grain = grainFactory.GetGrain<HotReloadScenario.IHotReloadAddedGrain>("before");
grain.Ping().GetAwaiter().GetResult();
});
Copilot AI review requested due to automatic review settings September 13, 2026 15:01
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from 32c54de to fe5f676 Compare September 13, 2026 15:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate and critical findings affect refresh ordering, publication reliability, cache consistency, provider activation, and migratability.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (8)

src/Orleans.Core/Manifest/ClientHotReloadRefresher.cs:72

  • Writing the new local manifest here is visible to ClientClusterManifestProvider.RunAsync; if a gateway response is processed concurrently, it can publish a cluster manifest containing the new interfaces before _rpcProvider, the interface resolver, and _grainVersionManifest have been refreshed. That creates a transient window where callers see metadata newer than the client caches. Update the dependent caches first and publish the local client manifest last.
                _clientClusterManifestProvider.OnLocalManifestUpdated(_clientManifestProvider.ClientManifest);

src/Orleans.Runtime/Manifest/SiloHotReloadRefresher.cs:74

  • Publishing the refreshed provider does not update ManagementGrain, which captures both LocalGrainManifest and Current in readonly fields when its activation is created (src/Orleans.Runtime/Core/ManagementGrain.cs:46-47). Any existing management activation will therefore continue rejecting GetActivationAddress and interface checks for grain metadata added by hot reload. Make management operations read the live provider or refresh/recreate those snapshots as part of this update.
                _clusterManifestProvider.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);

src/Orleans.Runtime/Manifest/SiloHotReloadRefresher.cs:77

  • OnLocalManifestUpdated makes the new manifest visible to ClusterManifestProvider before _rpcProvider, _grainInterfaceTypeToGrainTypeResolver, and _grainVersionManifest have been refreshed. A concurrent request (or a peer which observes the new minor version) can therefore use the new manifest while this silo still has the old proxy, interface-resolution, or version state. Refresh these local consumers first and publish the cluster manifest last so the version bump is the visibility boundary.
                _clusterManifestProvider.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);
                _rpcProvider.OnManifestUpdated();
                _grainInterfaceTypeToGrainTypeResolver.OnManifestUpdated();
                _grainVersionManifest.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);

src/Orleans.Serialization/Hosting/SerializationHotReloadRefresher.cs:79

  • TypeManifestProviderAttribute is also valid for user-authored providers, and the provider-discovery contract keeps those providers out of reflection activation because they are configured through DI (test/Orleans.Core.Tests/ProviderRegistrationResolverTests.cs:81-103). This refresh path calls Activator.CreateInstance for every attributed provider, so a custom provider with constructor dependencies is silently skipped and a parameterless one is instantiated outside its configured lifetime. Resolve registered providers through the service provider, or restrict reflective activation to generated providers.
                        if (Activator.CreateInstance(attribute.ProviderType, nonPublic: true) is IConfigureOptions<TypeManifestOptions> provider)
                        {
                            providers.Add(provider);
                        }

src/Orleans.Serialization/Hosting/SerializationHotReloadRefresher.cs:160

  • Generated hot-reload assemblies can contain open generic codec types such as Codec_GenericWithCtor<T> whose mutable getField_/setField_ fields are included by this scan. FieldInfo.SetValue cannot set a field on a declaring type which still contains generic parameters, so the first such field throws and the enclosing catch stops clearing accessors for every remaining generated type in the assembly. Handle generic type definitions separately (or clear through generated helpers) and continue per field/type instead of aborting the whole assembly.
                    if (field.Name.StartsWith("setField_", StringComparison.Ordinal) || field.Name.StartsWith("getField_", StringComparison.Ordinal))
                    {
                        field.SetValue(null, null);

src/Orleans.Serialization/Serializers/CodecProvider.cs:115

  • These caches are cleared without coordinating with in-flight TryGetCodec/copier calls. A call which started before the manifest swap can finish creating an AbstractTypeSerializer (or another fallback) and execute GetOrAdd after this clear, leaving the stale result cached and causing the newly registered generated codec to remain unused until a later refresh. Publish caches with a generation check or synchronize cache creation with manifest refresh so pre-refresh work cannot repopulate the new cache.
            _untypedCodecs.Clear();
            _typedCodecs.Clear();
            _typedBaseCodecs.Clear();
            _untypedCopiers.Clear();
            _typedCopiers.Clear();

src/Orleans.Serialization/TypeSystem/TypeConverter.cs:170

  • An in-flight Parse can pass the filter while the type is still absent, then be preempted before InspectTypeNameFilters writes its false verdict. If refresh runs and purges the dictionary during that gap, the old parse resumes and reinserts the negative entry, so the newly manifested type can still be rejected after refresh. Use a copy-on-write/versioned allow-list cache or synchronize verdict writes with OnManifestUpdated.
            foreach (var entry in _allowedTypes)
            {
                if (!entry.Value)
                {
                    _allowedTypes.TryRemove(entry.Key, out _);

src/Orleans.Serialization/TypeSystem/TypeConverter.cs:153

  • ConsumeMetadata(options) formats newly registered serializer and invokable types, and those generated invokables can use a new CompoundTypeAlias branch (for example, the alias generated for a newly added grain interface). ResolveCompoundAliasType still reads the previous _compoundTypeAliases tree during that call, so it can throw TypeLoadException before the refresh publishes the new manifest. Make the new alias tree available to metadata consumption without exposing a partially applied state.
        _compoundTypeAliases = options.CompoundTypeAliases;
  • Files reviewed: 27/27 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment on lines +73 to +74
_siloManifestProvider.OnManifestUpdated();
_clusterManifestProvider.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);
Comment on lines +80 to +94
for (var attempt = 0; attempt < 3; attempt++)
{
var current = _current;
if (current.Silos.TryGetValue(_localSiloAddress, out var existing) && ReferenceEquals(existing, localManifest))
{
return;
}

var version = new MajorMinorVersion(current.Version.Major, current.Version.Minor + 1);
var updated = CreateClusterManifest(version, current.Silos.SetItem(_localSiloAddress, localManifest));
if (TryPublishManifest(updated))
{
return;
}
}
Comment on lines +93 to +98
#if NET6_0_OR_GREATER
if (System.Reflection.Metadata.MetadataUpdater.IsSupported)
{
// Construct the hot reload refresher (if registered) so it subscribes before the first update.
_ = _serviceProvider.GetService<Hosting.SerializationHotReloadRefresher>();
}
Copilot AI review requested due to automatic review settings September 16, 2026 10:09
@ReubenBond
ReubenBond force-pushed the feature/hot-reload-manifest-refresh branch from fe5f676 to 48556ba Compare September 16, 2026 10:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Ten unresolved moderate findings affect concurrency, cache refresh, manifest publication, and configuration preservation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (10)

src/Orleans.Core/Core/GrainInterfaceTypeToGrainTypeResolver.cs:42

  • Clear() can race with the write at the end of an in-flight generic lookup: a call which captured the old cluster manifest can resume after this line and add its old result back into _genericMapping. A subsequent lookup can then use that stale mapping even though the refreshed manifest contains a different implementation. Publish a generation-aware cache or prevent lookups from inserting results from an older manifest.
        internal void OnManifestUpdated() => _genericMapping.Clear();

src/Orleans.Core/Manifest/ClientHotReloadRefresher.cs:67

  • Calling DefaultGrainTypeOptionsProvider.Configure on the already-final _grainTypeOptions also repopulates interfaces which a client configuration intentionally removed. For example, test/Orleans.Core.Tests/ClientBuilderTests.cs:149-160 clears options.Interfaces to reject startup; after this refresh a hot-reload event adds all generated interfaces back, changing the client's configured manifest and proxy surface. Rebuild a scratch options object through the complete configuration pipeline, or otherwise preserve existing exclusions while merging new interfaces.
                        defaultProvider.Configure(_grainTypeOptions);

src/Orleans.Runtime/Manifest/ClusterManifestProvider.cs:104

  • If a membership or peer-repair publication wins all three races, this method simply returns without publishing localManifest. LocalGrainManifest has already been replaced, but Current still contains the old local manifest and no later path necessarily retries because the membership major version is unchanged; peers can therefore permanently miss the newly added grain metadata. The update needs a serialized publication/reconciliation path or a retry which cannot silently give up.
            // Publish with a retry: a concurrently published manifest (e.g. built from membership processing
            // before this update) can win the race while still carrying the previous local manifest.
            for (var attempt = 0; attempt < 3; attempt++)
            {
                var current = _current;

src/Orleans.Runtime/Manifest/SiloHotReloadRefresher.cs:69

  • Calling DefaultGrainTypeOptionsProvider.Configure directly on the already-final _grainTypeOptions repopulates every generated grain class that startup configuration intentionally removed. GrainTypeOptions is used for per-silo exclusions such as options.Classes.Remove(...) and even options.Classes.Clear() (for example, test/Benchmarks/Ping/PingBenchmark.cs:37-40 and test/Orleans.Core.Tests/SiloBuilderTests.cs:295-304), so any hot-reload update can silently change activation and placement behavior. Rebuild a scratch options object through the complete configuration pipeline, or otherwise merge only newly discovered entries while preserving existing exclusions.
                        defaultProvider.Configure(_grainTypeOptions);

src/Orleans.Runtime/Manifest/SiloHotReloadRefresher.cs:74

  • GrainMigratabilityChecker is a singleton which captures clusterManifestProvider.LocalGrainManifest at construction and caches the result of IsImmovable. Refreshing SiloManifestProvider and ClusterManifestProvider here does not update that checker, so a newly added ordinary grain is absent from the captured manifest, classified as immovable, and cached as non-migratable for rebalancing/repartitioning. The refresh must invalidate or update this consumer as well.
                _siloManifestProvider.OnManifestUpdated();
                _clusterManifestProvider.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);

src/Orleans.Serialization/Hosting/HotReloadMetadataUpdateHandler.cs:64

  • The new tests call SerializationHotReloadRefresher.Refresh and SiloHotReloadRefresher.Refresh directly, so UpdateApplication itself is never exercised. That leaves the assembly filtering, phase ordering, weak-participant snapshot, and the documented no-throw behavior unverified; add a focused handler test which supplies manifest and non-manifest updates and a throwing participant.
    public static void UpdateApplication(Type[]? updatedTypes)
    {
        try

src/Orleans.Serialization/Hosting/SerializationHotReloadRefresher.cs:18

  • This changes the supported restart boundary, but the current guidance in docs/site/src/content/docs/grains/code-generation.md:45-47 still says a restart is required when an update introduces types through polymorphic or container members and does not describe the new grain-interface/class workflow. Update the hot-reload documentation to cover the refreshed manifest behavior and its stated limitations before merging.
/// Refreshes the serialization layer after a .NET Hot Reload update: re-runs the updated assemblies'
/// generated manifest providers, merges the result into the live <see cref="TypeManifestOptions"/>, and
/// refreshes the caches derived from it.

src/Orleans.Serialization/Serializers/CodecProvider.cs:97

  • This lookup is the only place a serializer-only container subscribes the refresher, but it runs only from Initialize, which is reached on the first codec lookup. An application can construct Serializer/CodecProvider, receive a metadata update, and only then perform its first serialization; no participant is registered for that update, so the live manifest is never merged and the newly generated type remains unavailable. The refresher needs to be subscribed before metadata updates, independently of first codec use.
                    _ = _serviceProvider.GetService<Hosting.SerializationHotReloadRefresher>();

src/Orleans.Serialization/TypeSystem/TypeCodec.cs:43

  • ConcurrentDictionary.Clear() does not fence an in-flight GetOrAdd factory. A type-name lookup which started before the manifest update can finish after these clears and insert the old TypeKey, so subsequent writes can continue using the pre-refresh alias/name indefinitely. Cache population needs to be synchronized with refresh or versioned so stale factories cannot repopulate the current cache.
        internal void ClearCaches()
        {
            _typeCache.Clear();
            _typeKeyCache.Clear();

src/Orleans.Serialization/TypeSystem/TypeConverter.cs:170

  • Removing the current negative entries is not sufficient when parsing can run concurrently with this refresh. A parse which observed the old denying filter can set _allowedTypes[type] = false after this loop has removed it, leaving the stale denial in place and causing the newly allowed type to remain rejected. Coordinate the purge with verdict publication or associate cached verdicts with a manifest generation.
            foreach (var entry in _allowedTypes)
            {
                if (!entry.Value)
                {
                    _allowedTypes.TryRemove(entry.Key, out _);
  • Files reviewed: 27/27 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +73 to +77
_siloManifestProvider.OnManifestUpdated();
_clusterManifestProvider.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);
_rpcProvider.OnManifestUpdated();
_grainInterfaceTypeToGrainTypeResolver.OnManifestUpdated();
_grainVersionManifest.OnLocalManifestUpdated(_siloManifestProvider.SiloManifest);
Comment on lines +109 to +113
ConsumeMetadata(metadata);

_untypedCodecs.Clear();
_typedCodecs.Clear();
_typedBaseCodecs.Clear();

This branch has not been deployed

No deployments
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.

3 participants