Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
3ad433b
fix(serialization): statically register native RPC responses
ReubenBond Oct 1, 2026
437445f
fix(codegen): register polymorphic native response dispatch
ReubenBond Oct 1, 2026
5470d3f
fix(codegen): require explicit dictionary response comparer contracts
ReubenBond Oct 1, 2026
c7e81df
fix(codegen): root generated response models in metadata mode
ReubenBond Oct 1, 2026
3d21ca5
fix(codegen): define finite native response transport contracts
ReubenBond Oct 1, 2026
5ae4525
fix(codegen): emit factories for completion-only RPC contracts
ReubenBond Oct 1, 2026
d4fc48c
fix(serialization): preserve referenced factory contracts and alias p…
ReubenBond Oct 1, 2026
3f04a21
fix(codegen): generate response factories for instance RPC methods
ReubenBond Oct 1, 2026
6986876
fix(serialization): preserve complete alias metadata and legacy trave…
ReubenBond Oct 2, 2026
047a56a
fix(codegen): close canonical response model construction dependencies
ReubenBond Oct 2, 2026
bb66cbe
fix(codegen): reject incomplete custom activator construction roots
ReubenBond Oct 2, 2026
8eb26f8
fix(codegen): close canonical value and array serializer services
ReubenBond Oct 2, 2026
e255a7b
fix(codegen): close canonical tuple and RPC argument construction ser…
ReubenBond Oct 2, 2026
0bbfb9b
test(codegen): update canonical tuple response registration snapshot
ReubenBond Oct 2, 2026
3576c82
feat(rpc): generate self-writing copied response holders
ReubenBond Oct 2, 2026
1c295fc
test(nativeaot): round-trip self-writing primitive responses
ReubenBond Oct 2, 2026
dc81537
fix(rpc): preserve custom selection for generated default graphs
ReubenBond Oct 2, 2026
e10fcfe
fix(serialization): use matching metadata for default eligibility
ReubenBond Oct 2, 2026
9aee664
test(codegen): refresh canonical default contract snapshots
ReubenBond Oct 2, 2026
e98285e
fix(codegen): remove generated field-accessor markers
ReubenBond Oct 2, 2026
f6a0ec2
fix(codegen): close canonical pair construction dependencies
ReubenBond Oct 2, 2026
82d6ed1
fix(codegen): close RPC constructor service dependencies
ReubenBond Oct 3, 2026
250b456
docs(serialization): align missing response diagnostics
ReubenBond Oct 3, 2026
15ffa59
fix(codegen): gate concrete RPC factories independently of aliases
ReubenBond Oct 3, 2026
6878370
fix(codegen): specialize canonical generic constructor contracts
ReubenBond Oct 3, 2026
c661517
fix(codegen): diagnose aggregate RPC graph admission failures
ReubenBond Oct 3, 2026
f5d01d2
fix(messaging): restrict raw response writers to response bodies
ReubenBond Oct 3, 2026
e5515fc
fix(codegen): disambiguate generated RPC response names
ReubenBond Oct 3, 2026
ad4eec5
fix(rpc): preserve isolated response ownership through send
ReubenBond Oct 3, 2026
6163d12
fix(serialization): decline external dependencies in inferred graphs
ReubenBond Oct 3, 2026
1a99842
fix(codegen): release compatibility response leases
ReubenBond Oct 3, 2026
e16ed81
fix(rpc): restore post-filter isolation and close generic activators
ReubenBond Oct 5, 2026
ff058c0
fix(nativeaot): integrate rebased response factories and transport
ReubenBond Oct 5, 2026
9735753
fix(rpc): retain superseded filter response leases
ReubenBond Oct 6, 2026
2b55277
fix(nativeaot): preserve rooted serializer materialization
ReubenBond Oct 6, 2026
c032496
fix(codegen): integrate runtime-independent RPC registrations
ReubenBond Oct 6, 2026
ecdf3d9
fix(rpc): preserve host construction and clarify argument admission
ReubenBond Oct 6, 2026
24f2ae5
chore(api): regenerate all admitted RPC response surfaces
ReubenBond Oct 6, 2026
dd85768
fix(serialization): preserve converter priority for inferred defaults
ReubenBond Oct 6, 2026
f8c2be2
fix(codegen): bind metadata-only RPC compilations
ReubenBond Oct 6, 2026
e572455
fix(rpc): honor explicit child factories
ReubenBond Oct 6, 2026
8c39f2d
feat(networking): add Linux io_uring transport
ReubenBond Aug 27, 2026
ef5ef90
feat(networking): enable io_uring with a runtime switch
ReubenBond Aug 27, 2026
53083b1
perf(networking): shard io_uring engines
ReubenBond Aug 27, 2026
a3ec72f
perf(networking): use io_uring sendmsg on loopback
ReubenBond Aug 27, 2026
0de0c5e
perf(networking): use stable io_uring operation slots
ReubenBond Aug 27, 2026
20e6d28
test(networking): stress io_uring operation reuse
ReubenBond Aug 27, 2026
57dfae4
perf(networking): adapt io_uring continuation dispatch
ReubenBond Aug 27, 2026
32297a6
fix(networking): tolerate io_uring CQ overflow
ReubenBond Aug 27, 2026
6aafd8e
fix(networking): harden io_uring lifecycle cleanup
ReubenBond Aug 27, 2026
333a332
test(networking): cover io_uring zero-copy notifications
ReubenBond Aug 27, 2026
8c8447e
perf(networking): reserve io_uring SQEs directly
ReubenBond Aug 27, 2026
a66bd1d
perf(networking): advance io_uring CQEs directly
ReubenBond Aug 27, 2026
83cbeb4
perf(networking): skip ready io_uring waits
ReubenBond Aug 27, 2026
2f1dc63
perf(networking): colocate small io_uring sends
ReubenBond Aug 27, 2026
efb4a1d
feat(serialization): adopt received buffer pages
ReubenBond Aug 27, 2026
0b9d907
feat(networking): add incremental io_uring receives
ReubenBond Aug 27, 2026
df89072
perf(networking): vectorize large io_uring sends
ReubenBond Aug 27, 2026
c62c4f5
perf(networking): adapt io_uring zero-copy sends
ReubenBond Aug 27, 2026
0482feb
feat(networking): support io_uring NAPI polling
ReubenBond Aug 27, 2026
d087f5e
revert(networking): remove vectorized io_uring sends
ReubenBond Aug 27, 2026
33361f2
perf(networking): streamline io_uring buffer state
ReubenBond Aug 28, 2026
30572b2
fix(networking): complete io_uring rebase integration
ReubenBond Aug 28, 2026
0237eae
fix(test): propagate io_uring test cancellation
ReubenBond Aug 28, 2026
2d9fe7d
perf(networking): complete small sends synchronously
ReubenBond Aug 28, 2026
b374034
perf(networking): adapt io_uring for tiny messages
ReubenBond Aug 28, 2026
97dad1f
fix(networking): guard synchronous send lifecycle
ReubenBond Aug 28, 2026
c3813d9
fix(networking): preserve synchronous send ownership
ReubenBond Aug 28, 2026
2a39821
fix(networking): harden transport listener setup
ReubenBond Aug 28, 2026
b89553c
fix(networking): address transport review feedback
ReubenBond Sep 2, 2026
d038289
fix(networking): complete io_uring rebase integration
ReubenBond Sep 4, 2026
60e267d
style(networking): format io_uring transport
ReubenBond Sep 4, 2026
10c25dd
fix(networking): preserve io_uring ownership after rebase
ReubenBond Sep 4, 2026
f910b24
fix(networking): handle optional gateways safely
ReubenBond Sep 4, 2026
c6aa67a
fix(networking): harden socket transport shutdown
ReubenBond Sep 4, 2026
c018275
fix(networking): preserve io_uring ownership on upstream main
ReubenBond Oct 6, 2026
43a6ca3
perf(serialization): avoid allocating on published service lookups
ReubenBond Oct 7, 2026
05c9d2d
perf(serialization): reuse default admission traversal state
ReubenBond Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/site/src/content/docs/implementation/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,42 @@ For a return type marked through <xref:Orleans.Invocation.ReturnValueProxyAttrib

Arguments and result values use normal Orleans.Serialization codecs and copiers. Exceptions are represented by exception responses and rethrown by the caller completion source. A grain call with a `CancellationToken` exposes cancellation through the generated request, allowing the runtime to propagate cooperative cancellation. Void methods set the one-way invocation option in their request base, so no response completion source waits for a result.

### Closed RPC response factories

For supported non-generic methods using the built-in task and value-task request bases, generated invokables also implement <xref:Orleans.Serialization.Invocation.IResponseInvokable>. The runtime selects this contract in the ordinary, incoming-filter, and observer paths. The invokable binds the selected serialization dependencies and rents a concrete, non-generic response holder after copying a mutable result. Immutable results retain their existing copy semantics. Isolation completes before incoming filters resume, and this path creates one pooled envelope for the successful result.

Incoming filters receive an isolated result and can transform its envelope or nested payload references. The invocation context retains each superseded response until completion, allowing filters to restore an earlier selection and releasing every unselected wrapper exactly once. After the filter chain completes, the runtime copies the selected result for delivery, preserving isolation for both local callers and deferred message serialization. Calls with no incoming filters transfer their invocation-time copy directly to the messaging pipeline. Expired and one-way requests release their owned response without an outgoing copy.

Generated holders implement <xref:Orleans.Serialization.Invocation.IRawResponseWriter>. The message serializer selects their direct writer before runtime-type codec lookup. Primitive holders call the existing static primitive codecs; reference holders use their bound concrete result codec and the message's serialization session. The wire representation retains the result-type header, field zero, end marker, null behavior, cycles, and shared references. Returning a holder to its pool clears both its result and its provider-owned factory binding.

The receiving message serializer first consults <xref:Orleans.Serialization.Serializers.CodecProvider.TryGetRawResponseReader*> using the result type from the wire header. Generated <xref:Orleans.Serialization.Invocation.IRawResponseReader> registrations reconstruct a bound holder directly. Compatibility paths retain ordinary response codecs for custom response/payload implementations, custom invokable bases, and unresolved generic contracts. Generated direct factories activate only when the selected result and response codecs/copiers match the canonical implementations, so explicit application behavior retains precedence.

For concrete `Task<TResult>` and `ValueTask<TResult>` method results, the generator emits identical invocation code and closed response registrations for JIT and NativeAOT execution. Provider registration precedence selects the services in both execution modes. The graph uses the concrete result implementations in <xref:Orleans.Serialization.Invocation.PooledResponseCodec`2> and <xref:Orleans.Serialization.Invocation.PooledResponseCopier`2>. Recursive result dependencies resolve with a construction caller after the response implementation has been allocated. Reference payload codecs and copiers preserve payload cycles and shared references, and the pooled response envelope retains the existing field and raw-message encoding.

The graph also registers polymorphic codec and copier dispatch for the non-generic <xref:Orleans.Serialization.Invocation.Response> boundary used by the runtime client. That dispatch selects the closed implementation for the actual response type and preserves the identity of immutable completed and exception responses. The native smoke uses <xref:Orleans.Serialization.DeepCopier`1> with `Response`, matching the runtime's response-copy boundary.

Completed response transport uses the existing generated codec and its canonical singleton activator, restoring <xref:Orleans.Serialization.Invocation.CompletedResponse.Instance> after a round-trip. Interfaces containing only non-generic `Task` or `ValueTask` methods also generate this shared response/completion graph.

The finite response graph supplies successful typed results and completed-response transport, plus immutable exception-envelope copying. Exception transport uses the selected <xref:Orleans.Serialization.Invocation.ExceptionResponse> codec and the exception and `Data` value dependencies. Closed factories provide statically compiled implementations; registered metadata and the existing exception codecs provide the remaining supported serialization contracts.

These supplemental registrations are emitted unconditionally and are defaults: explicit closed factory registrations take precedence in either configuration order. Inferred parent factories preserve each child's selected service: replacing a child contract selects parent construction through that contract, while parents consuming the explicit contract directly remain eligible. This applies throughout the dependency graph, including concrete canonical services and their codec, copier, and activator aliases. Generated factories and registered metadata use one resolution pipeline, with closed services selected first. Direct-holder factories and raw-reader registrations bind the selected provider implementations in both JIT and native execution. Custom application codec/copier selection uses the compatibility invocation and serialization path.

Generated metadata also supplies static response factories for closed generated result models. These factories construct the model's canonical generated codec and copier using their actual constructor signatures, including closed generic generated activators and available generated activators from referenced assemblies. Source-known arrays, tuples, collections, and surrogate value serializers contribute their closed construction services. Interface contracts retain registered metadata dispatch and propagate their dependency requirements. Reference-assembly construction uses the producer's available constructor contracts and explicitly identified members.

Inferred defaults participate when their complete construction graph uses closed service factories and provider-owned services. Admission compares matching metadata implementation identities, and the closed factories supply the executable services. Automatic metadata construction retains ordinary dependency-injection resolution throughout its call chain. Closed default roots start their own construction transactions, and explicit factories participate in the declared transaction. Constructor dependencies supplied by ordinary dependency-injection registrations select canonical metadata activation before the provider starts a serialization construction transaction. <xref:Orleans.Serialization.Configuration.TypeManifestOptions.AddDefaultSerializerService*> carries these dependency edges, preserving explicit registration priority, canonical service identity, and graph rollback. Default activation uses the existing reference- or value-type activator implementation, and custom activation retains its declared contract. Serializer contexts contribute their complete finite dependency graphs and validate each declared member shape.

The same collector closes source-known argument construction dependencies selected by generated proxy constructors. Reference and value tuples use their existing closed codec and copier implementations with their declared element services. Parameter-only one-way contracts register the required construction services while completion and result contracts also register their response graphs.

`OrleansValidateRpcResponseFactories` enables compile-time validation of the response graph and defaults to the executable project's `PublishAot` setting. Diagnostic `ORLEANS0116` identifies an unresolved generic result, a custom return adapter requiring an explicit response contract, or a result dependency outside the supported finite graph. Applications with runtime-selected generic results register every permitted closed <xref:Orleans.Serialization.Invocation.Response`1> graph explicitly through <xref:Orleans.Serialization.Configuration.TypeManifestOptions.AddSerializer*> and <xref:Orleans.Serialization.Configuration.TypeManifestOptions.AddSerializerService*> in a <xref:Orleans.Serialization.SerializerContext>, and set `OrleansValidateRpcResponseFactories=false` for the project supplying that contract. A missing native response registration reports the closed response type and requested serialization service at lookup.

The combined inferred response graph supports up to 1,024 closed executable types. RPC admission counts the codec/copier construction graph; explicit serializer contexts also validate their separate metadata traversal limits. A larger union produces `ORLEANS0116` in both managed and native builds, even when each method's graph fits individually. The generator retains response holder declarations so the size diagnostic identifies the admission failure directly. Argument admission failures use an argument-specific `ORLEANS0116` message directing the application to register the argument's closed codec, copier, and serialization service dependencies, including for one-way methods.

Dictionary results and dictionary members require an explicit closed registration which preserves the application's comparer contract. A dictionary's comparer is selected per value, so the method's declared result type alone supplies the key/value shape while the registration supplies comparer serialization and copying.

The focused .NET 10 NativeAOT smoke exercises the generated response graph for boolean, integer, and reference results, including recursive factory dependencies and payload identity. Full silo startup and RPC execution additionally require the native support for activation, request serialization, grain references, and runtime metadata. Managed .NET 8 tests exercise JIT compatibility and compiled-reference contracts.

Source: [RPC response factory generation](https://github.com/dotnet/orleans/blob/main/src/Orleans.CodeGenerator/RpcResponseGenerator.cs), [closed serializer factory graphs](https://github.com/dotnet/orleans/blob/main/src/Orleans.CodeGenerator/SerializerFactoryGenerator.cs), and [native response smoke](https://github.com/dotnet/orleans/blob/main/test/Orleans.NativeAotSmoke/RpcResponses.Contracts.cs).

### Request identity and dispatch

Generated request names are implementation details. Their wire identity is a compound alias containing:
Expand Down
1 change: 1 addition & 0 deletions src/Orleans.CodeGenerator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ ORLEANS0112 | Usage | Error | Invalid RPC parameter field identifier
ORLEANS0113 | Usage | Warning | CancellationToken parameter is not last
ORLEANS0114 | Usage | Error | Invalid serializer context declaration
ORLEANS0115 | Usage | Error | Unsupported serializer context dependency
ORLEANS0116 | Usage | Error | RPC response requires a closed serializer factory
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ internal static class DiagnosticRuleId
public const string CancellationTokenNotLast = "ORLEANS0113";
public const string InvalidSerializerContext = "ORLEANS0114";
public const string UnsupportedSerializerContextType = "ORLEANS0115";
public const string UnsupportedRpcResponseFactory = "ORLEANS0116";
}
65 changes: 56 additions & 9 deletions src/Orleans.CodeGenerator/InvokableGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,50 @@ private ClassDeclarationSyntax GetClassDeclarationSyntax(
GenerateTryCancelMethod(method, fieldDescriptions),
GenerateIsCancellableProperty(method));

if (method.AllTypeParameters.Count == 0
&& method.Method.ReturnType is INamedTypeSymbol { TypeArguments.Length: 1 } result
&& baseClassType.OriginalDefinition.ToDisplayString() is "Orleans.Runtime.TaskRequest<TResult>" or "Orleans.Runtime.Request<TResult>"
&& _generationContext.RpcResponseNames.TryGetValue(
result.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), out var responseName))
{
var type = result.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var factory = $"global::{RpcResponseHolderGenerator.GetNamespace(_generationContext.Compilation)}.{responseName}Factory";
classDeclaration = classDeclaration.AddBaseListTypes(SimpleBaseType(ParseTypeName("global::Orleans.Serialization.Invocation.IResponseInvokable")));
classDeclaration = classDeclaration.AddMembers(ParseMemberDeclaration($$"""
async global::System.Threading.Tasks.ValueTask<global::Orleans.Serialization.Invocation.Response>
global::Orleans.Serialization.Invocation.IResponseInvokable.InvokeAndCopy(
global::Orleans.Serialization.Serializers.ICodecProvider provider,
global::Orleans.Serialization.Cloning.CopyContextPool contexts,
global::Orleans.Serialization.DeepCopier<global::Orleans.Serialization.Invocation.Response> responseCopier)
{
try
{
var factory = {{factory}}.Resolve(provider);
if (!factory.IsSupported)
{
var original = await Invoke();
global::Orleans.Serialization.Invocation.Response copy = null;
try
{
return copy = responseCopier.Copy(original);
}
finally
{
if (!global::System.Object.ReferenceEquals(original, copy))
original.Dispose();
}
}
{{type}} value = await InvokeInner();
return factory.RentCopied(value, contexts);
}
catch (global::System.Exception exception)
{
return global::Orleans.Serialization.Invocation.Response.FromException(exception);
}
}
""")!);
}

if (method.AllTypeParameters.Count > 0)
{
classDeclaration = SyntaxFactoryUtility.AddGenericTypeParameters(classDeclaration, method.AllTypeParameters);
Expand Down Expand Up @@ -738,21 +782,24 @@ MemberDeclarationSyntax GetFieldDeclaration(InvokerFieldDescription description)
{
var methodTypeArguments = GetTypesArray(method, method.MethodTypeParameters.Select(p => p.Parameter));
var parameterTypes = GetTypesArray(method, method.Method.Parameters.Select(p => p.Type));
var methodLookup = method.AllTypeParameters.Count == 0
? ParseExpression($"typeof({method.Method.ContainingType.ToTypeSyntax(method.TypeParameterSubstitutions)}).GetMethod({method.Method.Name.GetLiteralExpression()}, 0, global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, {(method.Method.Parameters.Length == 0 ? "global::System.Type.EmptyTypes" : parameterTypes.ToString())}, null)")
: InvocationExpression(
IdentifierName("OrleansGeneratedCodeHelper").Member("GetMethodInfoOrDefault"),
ArgumentList(SeparatedList(
[
Argument(TypeOfExpression(method.Method.ContainingType.ToTypeSyntax(method.TypeParameterSubstitutions))),
Argument(method.Method.Name.GetLiteralExpression()),
Argument(methodTypeArguments),
Argument(parameterTypes),
])));

field = FieldDeclaration(
VariableDeclaration(
LibraryTypes.MethodInfo.ToTypeSyntax(),
SingletonSeparatedList(VariableDeclarator(description.FieldName)
.WithInitializer(EqualsValueClause(
InvocationExpression(
IdentifierName("OrleansGeneratedCodeHelper").Member("GetMethodInfoOrDefault"),
ArgumentList(SeparatedList(
[
Argument(TypeOfExpression(method.Method.ContainingType.ToTypeSyntax(method.TypeParameterSubstitutions))),
Argument(method.Method.Name.GetLiteralExpression()),
Argument(methodTypeArguments),
Argument(parameterTypes),
]))))))))
methodLookup)))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.ReadOnlyKeyword));
}
else
Expand Down
Loading
Loading